Inventory Management API is a Node.js and Express backend for managing products, warehouses, stock records and inventory workflows.
The project focuses on realistic backend concerns such as authentication, role-based access control, controlled user provisioning, bulk inventory operations and stock movement history. It is built as a backend portfolio project and not as a full warehouse management system.
Phase 1 release-hardened backend portfolio project with documented operational limitations.
The goal of this project is to model a practical inventory backend with real business rules instead of only simple CRUD endpoints.
The API covers:
- product and warehouse management
- stock records for product/warehouse combinations
- goods receipt and goods issue workflows
- stock movement history
- role-based access control
- controlled user creation
- bulk operations for import and automation scenarios
The project is suitable for integration-focused use cases such as internal admin tools, warehouse workflows, and controlled automation clients.
The API is deployed on Render and connected to MongoDB Atlas.
https://inventory-management-api-6zuo.onrender.com
Swagger UI is available at:
https://inventory-management-api-6zuo.onrender.com/api-docs
Health check:
https://inventory-management-api-6zuo.onrender.com/health
Note: The service runs on Render's free plan, so the first request after inactivity may take a few seconds.
- Node.js
- Express.js
- MongoDB
- Mongoose
- dotenv
- bcrypt
- jsonwebtoken
- express-validator
- helmet
- express-rate-limit
- pino
- swagger-jsdoc
- swagger-ui-express
- @apidevtools/swagger-parser
- Docker
- Docker Compose
- Jest
- Supertest
- mongodb-memory-server
- GitHub Actions
- Render
- MongoDB Atlas
The executable server awaits the required MongoDB connection before opening the HTTP listener. A failed startup never marks the runtime ready and exits with a non-zero status without leaving a listener active.
GET /health/liveis the canonical liveness check and does not query or depend on MongoDB.GET /healthremains its backward-compatible alias.GET /health/readyis the canonical readiness check. It returns200only after startup and listening complete, MongoDB is connected, traffic is being accepted, and shutdown has not started.GET /api/readyis a compatibility readiness alias added by WP6.SIGTERMandSIGINTfirst make readiness unavailable, then close the HTTP server and MongoDB connection. The sequence is idempotent and bounded to ten seconds; timeout forces remaining HTTP connections closed and exits non-zero.- Runtime and HTTP terminal logs are one JSON object per line on standard output. They contain only allowlisted operational fields. Request/response bodies, query values, credentials, cookies, tokens, raw idempotency keys, user profile fields, database URIs, and production stack traces are excluded.
- A valid inbound
X-Request-IDorX-Correlation-IDis preserved. Missing or invalid values are replaced safely, correlation defaults to the effective request ID, and every response exposes both effective headers.
- JWT-based login
- Login rate limiting for repeated failed login attempts
- Refresh token workflow
- Refresh token hashing and rotation
- Older refresh tokens are revoked after a new successful login under normal operation
- Logout with refresh token revocation
- Current user endpoint
- No public user registration
- Initial admin creation through seed script
- Admin-only user creation
- Admins can create
managerandviewerusers - Creating another admin through the API is not allowed
The API uses three roles:
| Role | Description |
|---|---|
admin |
Full access, including user creation and product deletion |
manager |
Can manage inventory data and inventory workflows |
viewer |
Read-only access to inventory data |
- Helmet-based security headers
X-Powered-Byheader disabled- Public Swagger UI for portfolio/demo visibility
- Protected API operations still require Bearer authentication
- Required startup environment validation
- Production JWT placeholder and length rejection before database startup
- Repository secret-policy verification and production dependency audit gates
- Bounded MongoDB connection retry handling
- Production 5xx responses hide internal error details
- OpenAPI specification validation in the automated test suite
- Formal OpenAPI and sensitive-content validation
- Docker build, Compose configuration, and isolated non-root runtime smoke gates
- Product management
- Warehouse management
- Stock records for product/warehouse combinations
- Goods receipt workflow for increasing stock
- Goods issue workflow for decreasing stock
- Goods receipt and goods issue reject inactive stock records
- Product and Warehouse inactivity is enforced by inventory workflows
- Legacy Product DELETE routes archive documents and preserve references
- Explicit Product, Warehouse and Stock aggregate versions
- Transactionally synchronized Stock lifecycle guards
- Stock movement parent snapshots, exact before/after quantities and aggregate versions
- Conditional stock updates to reduce normal overselling risk
- Transactional Stock and StockMovement persistence for inventory workflows
- Read-only stock movement history
- Bulk operations for products, warehouses, stocks and inventory workflows
- Append-only, allowlisted AuditEvent records for successful Inventory Core mutations
- Transactional OutboxEvent records for every actual aggregate version transition
- Atomic keyed and unkeyed mutation, movement, audit and outbox persistence
Bulk endpoints are included for practical import and automation scenarios.
Supported bulk operations:
- bulk product creation
- bulk product update
- bulk Product archive through the deprecated DELETE compatibility route
- bulk warehouse creation
- bulk warehouse update
- bulk stock setup
- bulk goods receipt
- bulk goods issue
Bulk requests are limited to a maximum of 150 items per request.
/api/v1 is the canonical integration prefix. The same non-health operations
remain temporarily reachable through /api without duplicated controllers or
business logic. No legacy removal date is promised. Health and operational
routes remain unversioned: /, /health, /health/live, /health/ready, and
/api/ready.
Every successful v1 response uses this transport envelope:
{
"data": {},
"meta": {
"requestId": "request-id",
"correlationId": "correlation-id",
"schemaVersion": "1.0"
}
}Message-only successes use "data": null. Paginated collections add only
limit and nextCursor to meta; they do not execute or expose a total count.
Every v1 error has the following complete top-level contract:
{
"type": "inventory-error",
"title": "Validation failed",
"status": 400,
"code": "VALIDATION_FAILED",
"detail": "Validation failed",
"requestId": "request-id",
"correlationId": "correlation-id",
"retryable": false,
"errors": []
}The IDs in response bodies match X-Request-ID and X-Correlation-ID. Known
domain and idempotency codes remain machine-readable; validation details are
limited to 50 {field,message} entries and never include rejected values.
The Product, Warehouse, Stock, and StockMovement collection routes use opaque
cursor pagination under both prefixes. The default limit is 50 and the maximum
is 100. Sorting is limited to createdAt in asc or desc order (default
desc), with _id in the same direction as a deterministic tie-breaker. A
cursor is bound to its resource, normalized filters, sort, and order, so any
change invalidates it. V1 returns meta.nextCursor; legacy lists retain
{message,data} and expose a continuation only through X-Next-Cursor.
| Collection | Allowed filters | Normalization |
|---|---|---|
| Products | status, sku |
status enum; trimmed uppercase exact SKU of 1-64 characters; archived rows excluded |
| Warehouses | status, code |
status enum; trimmed uppercase exact code of 1-64 characters |
| Stocks | productId, warehouseId, status |
exact ObjectIds and status enum |
| Stock movements | stockId, productId, warehouseId, type, reference, from, to |
exact IDs/type/reference; inclusive ISO-8601 timestamps with timezone |
Unknown parameters, repeated/object values, MongoDB operators, arbitrary sort,
search, projection, population, and expansion are rejected. Public reads use
explicit projections and bounded Product/Warehouse summaries. Canonical v1
inventory mutations use operation-specific public DTO presenters after fresh or
replayed execution; legacy bodies and stored idempotency results remain
contract-neutral. Neither path exposes __v, private authentication data,
AuditEvent, OutboxEvent, or IdempotencyRecord data. Cursor pagination is
deterministic for a stable dataset but is not snapshot-isolated across
concurrent writes.
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/auth/login |
Login and receive access/refresh tokens |
POST |
/api/v1/auth/refresh |
Rotate refresh token and receive a new access token |
POST |
/api/v1/auth/logout |
Revoke refresh token |
GET |
/api/v1/auth/me |
Get the current authenticated user |
| Method | Endpoint | Role | Description |
|---|---|---|---|
POST |
/api/v1/users |
admin | Create a manager or viewer user |
Public registration is intentionally not available. The first admin user is created through the seed script.
| Method | Endpoint | Role | Description |
|---|---|---|---|
GET |
/api/v1/products |
admin, manager, viewer | Retrieve all products |
GET |
/api/v1/products/:id |
admin, manager, viewer | Retrieve a single product |
POST |
/api/v1/products |
admin, manager | Create a new product |
PATCH |
/api/v1/products/:id |
admin, manager | Update product information |
PATCH |
/api/v1/products/:id/deactivate |
admin, manager | Deactivate a product |
DELETE |
/api/v1/products/:id |
admin | Archive an inactive product (deprecated alias) |
POST |
/api/v1/products/bulk |
admin, manager | Create multiple products |
PATCH |
/api/v1/products/bulk |
admin, manager | Update multiple products |
DELETE |
/api/v1/products/bulk |
admin | Atomically archive inactive products (deprecated alias) |
| Method | Endpoint | Role | Description |
|---|---|---|---|
GET |
/api/v1/warehouses |
admin, manager, viewer | Retrieve all warehouses |
GET |
/api/v1/warehouses/:id |
admin, manager, viewer | Retrieve a single warehouse |
POST |
/api/v1/warehouses |
admin, manager | Create a new warehouse |
PATCH |
/api/v1/warehouses/:id |
admin, manager | Update warehouse information |
PATCH |
/api/v1/warehouses/:id/deactivate |
admin, manager | Deactivate a warehouse |
POST |
/api/v1/warehouses/bulk |
admin, manager | Create multiple warehouses |
PATCH |
/api/v1/warehouses/bulk |
admin, manager | Update multiple warehouses |
Warehouse deletion is intentionally not implemented because warehouses can be connected to stock records and movement history.
| Method | Endpoint | Role | Description |
|---|---|---|---|
GET |
/api/v1/stocks |
admin, manager, viewer | Retrieve all stock records |
GET |
/api/v1/stocks/:id |
admin, manager, viewer | Retrieve a single stock record |
POST |
/api/v1/stocks |
admin, manager | Create a stock record for a product and warehouse |
POST |
/api/v1/stocks/bulk |
admin, manager | Create multiple stock records |
Stock quantity is not updated directly through the stock API. Quantity changes are handled through goods receipt and goods issue workflows.
| Method | Endpoint | Role | Description |
|---|---|---|---|
POST |
/api/v1/goods-receipts |
admin, manager | Receive goods and increase stock quantity |
POST |
/api/v1/goods-receipts/bulk |
admin, manager | Process multiple goods receipts |
| Method | Endpoint | Role | Description |
|---|---|---|---|
POST |
/api/v1/goods-issues |
admin, manager | Issue goods and decrease stock quantity |
POST |
/api/v1/goods-issues/bulk |
admin, manager | Process multiple goods issues |
| Method | Endpoint | Role | Description |
|---|---|---|---|
GET |
/api/v1/stock-movements |
admin, manager, viewer | Retrieve stock movement history |
GET |
/api/v1/stock-movements/:id |
admin, manager, viewer | Retrieve a single stock movement |
Stock movements are generated by inventory workflows and exposed as read-only history. Manual stock movement creation is intentionally not exposed.
Before users can log in, create the first admin user:
npm run seed:adminThe command uses these environment variables:
ADMIN_NAME=Initial Admin
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=change_this_admin_passwordThe seed command is safe to run again. If an admin already exists, no new admin will be created.
{
"email": "admin@example.com",
"password": "change_this_admin_password"
}{
"name": "Warehouse Manager",
"email": "manager@example.com",
"password": "ChangeMe_Strong_123!",
"role": "manager"
}{
"sku": "LAPTOP-001",
"name": "Dell Latitude 7450",
"description": "Business laptop",
"unit": "piece"
}[
{
"sku": "PROD-001",
"name": "Product One",
"unit": "piece"
},
{
"sku": "PROD-002",
"name": "Product Two",
"unit": "piece"
}
]{
"code": "WH-STU",
"name": "Main Warehouse",
"description": "Primary warehouse for incoming and outgoing goods"
}[
{
"code": "WH-001",
"name": "Main Warehouse"
},
{
"code": "WH-002",
"name": "Secondary Warehouse"
}
]{
"productId": "PRODUCT_ID",
"warehouseId": "WAREHOUSE_ID"
}[
{
"productId": "PRODUCT_ID",
"warehouseId": "WAREHOUSE_ID"
},
{
"productId": "ANOTHER_PRODUCT_ID",
"warehouseId": "WAREHOUSE_ID"
}
]{
"stockId": "STOCK_ID",
"quantity": 10,
"reference": "PO-1001",
"reason": "Supplier delivery"
}[
{
"stockId": "STOCK_ID",
"quantity": 10,
"reference": "PO-1001",
"reason": "Supplier delivery"
},
{
"stockId": "STOCK_ID",
"quantity": 5,
"reference": "PO-1002",
"reason": "Second delivery"
}
]{
"stockId": "STOCK_ID",
"quantity": 3,
"reference": "SO-1001",
"reason": "Customer order"
}[
{
"stockId": "STOCK_ID",
"quantity": 3,
"reference": "SO-1001",
"reason": "Customer order"
},
{
"stockId": "STOCK_ID",
"quantity": 2,
"reference": "SO-1002",
"reason": "Second order"
}
]{
"refreshToken": "REFRESH_TOKEN"
}{
"refreshToken": "REFRESH_TOKEN"
}- Public registration is not available.
- The first admin user is created through
npm run seed:adminor the Docker Compose seed profile. - The seed script checks whether an admin already exists before creating one.
- Admin users can create
managerandviewerusers. - API users cannot create another admin through
/api/users. - Passwords are hashed before being stored.
- Refresh tokens are stored as hashes in the database.
- Refresh tokens are rotated when refreshing access tokens.
- Older refresh tokens are revoked after a new successful login under normal operation.
- Logout revokes the submitted refresh token.
- Login requests are rate-limited after repeated failed attempts.
- Each product must have a unique SKU.
- Product SKUs are normalized to uppercase.
- Product SKUs use a restricted identifier format.
- Each product must have a name.
- Product unit is limited to predefined values.
- New products are active by default.
- Products can be updated partially.
- Product and Warehouse updates expose an explicit
version;__vis not the domain version. - Update/deactivate requests may supply an optional positive-integer
expectedVersion. - Products must be deactivated before the legacy DELETE route can archive them.
- Archive retains the Product, prevents SKU reuse, preserves Stock and movement references, and hides the Product from normal Product reads and updates.
- Archived Products are terminal in the current API and have no restore endpoint.
Supported product units:
piece
kg
liter
meter
- Each warehouse must have a unique code.
- Warehouse codes are stored in uppercase and use a restricted identifier format.
- Warehouse codes are treated as business identifiers.
- Warehouse codes cannot be changed through update endpoints.
- Warehouses can be deactivated.
- Inactive Warehouses remain readable and may be reactivated through PATCH.
- Warehouses are not deleted.
- A stock record connects one product with one warehouse.
- The combination of product and warehouse must be unique.
- A stock record can only be created for an active product.
- A stock record can only be created for an active warehouse.
- Stock quantity starts at
0. - Stock carries derived Product and Warehouse lifecycle guards plus an explicit aggregate
version. - Stock creation conditionally touches each distinct parent version inside its transaction; that increment represents the new aggregate relationship.
- Stock quantity is not changed directly through the stock API.
- Stock quantity changes are handled through goods receipt and goods issue workflows.
- Goods receipt creates a stock movement and increases current stock quantity.
- Goods issue creates a stock movement and decreases current stock quantity.
- Goods issue is rejected when available quantity is insufficient.
- Goods receipt and goods issue are rejected for inactive stock records.
- Goods receipt and goods issue also reject inactive/archived Products, inactive Warehouses, missing parent references, and unresolved lifecycle guards.
- Goods issue uses conditional stock updates to reduce normal concurrent overselling risk.
- Goods receipt and goods issue commit Stock and StockMovement changes atomically.
- Bulk goods receipt and issue requests are all-or-nothing transactions.
- Every original movement item increments Stock version once. Repeated Stock IDs in bulk receive sequential before/after quantities and sequential
aggregateVersionvalues in request order. - New movements include direct Product/Warehouse references and immutable
{sku,name}/{code,name}snapshots. - Stock movements are read-only history.
- Manual stock movement creation is intentionally not exposed.
Stock quantity is not changed directly.
Inventory changes are handled through business workflows:
- Goods receipt
- Goods issue
- Stock movement history
The stock model connects products and warehouses. A stock record represents the quantity of one product in one warehouse.
Product
|
v
+-------+
| Stock |
+-------+
^
|
Warehouse
Stock changes are handled by:
Goods Receipt -> increases stock -> creates stock movement
Goods Issue -> decreases stock -> creates stock movement
This keeps the current inventory state and its movement history separated.
The project follows a simple layered backend structure.
Client
|
v
Routes
|
v
Authentication / RBAC
|
v
Validation
|
v
Controllers
|
v
Application Services
|
v
Transaction Helper (Inventory Mutations)
|
v
Models
|
v
MongoDB
Errors
|
v
Global Error Handler
Authentication is handled through JWT access tokens and refresh tokens.
Access tokens are used to protect private routes. Refresh tokens are stored as hashes in the database and can be revoked during logout or token rotation.
The application also applies basic security hardening through Helmet, disables the X-Powered-By header and limits repeated failed login attempts.
For automated testing, the Express application is separated from the server startup logic. src/app.js exports the Express app for tests, while src/server.js connects to the database and starts the HTTP server.
Server startup validates required environment variables before the application starts. MongoDB connection handling includes bounded retry attempts before failing the process.
Routes define the API endpoints and forward requests to the correct controller.
Validation rules check request payloads before controller logic is executed.
Controllers handle HTTP input and output. Goods receipt, goods issue, and Stock setup write controllers delegate their business behavior to application services.
The Inventory and Stock application services accept plain JavaScript inputs, apply the existing write rules, and use the Mongoose models. They do not depend on Express request or response objects.
Typed domain errors represent expected service failures internally. The global error handler preserves the existing public status codes and message-only error responses.
Goods receipt and goods issue services use one MongoDB transaction per request. Stock changes and their StockMovement records commit together, and each bulk request is all-or-nothing. The previous manual compensation updates were removed.
Every Inventory Core mutation now uses the same transaction for domain writes, StockMovement writes where applicable, AuditEvent records, OutboxEvent records, and keyed IdempotencyRecord completion. Each actual aggregate version transition creates one Audit/Outbox pair. Bulk and repeated Stock operations retain per-transition versions, including distinct Product/Warehouse parent touches on Stock creation. Successful no-ops create Audit only; idempotency replay creates no new events.
Audit snapshots and metadata are allowlisted plain JSON and limited to 16 KiB each, with canonical SHA-256 snapshot hashes. Event-specific Outbox payloads are limited to 64 KiB and start in pending delivery state. These records contain no raw credentials, tokens, request headers, user email/name/role, or raw Idempotency-Key. Audit and Outbox have no TTL. No event delivery worker or public event API is included; pending OutboxEvents accumulate until a future worker is implemented.
Product and Warehouse mutation controllers delegate lifecycle, explicit version, compare-and-swap, archive, and Stock guard propagation behavior to dedicated application services. Parent lifecycle and related Stock guard changes commit in one transaction. Inventory transactions validate both authoritative parents and derived Stock guards, then write that same Stock aggregate so concurrent parent lifecycle changes conflict safely.
version is the authoritative domain aggregate revision; Mongoose __v is not
the API concurrency contract. expectedVersion is optional on the current
legacy Product/Warehouse mutation APIs, so clients that omit it can still have
last-write-wins behavior. Mandatory preconditions are deferred to an approved
versioned API contract.
Transactions provide atomic database persistence and driver retry safety. Inventory Core mutation routes additionally support optional, actor-scoped idempotency for retry-safe HTTP execution. AuditEvent and OutboxEvent already exist and are persisted inside the same mutation transaction. Outbox delivery and machine identity remain future work. The canonical v1 HTTP contract is now implemented; mandatory optimistic preconditions remain a separate contract decision.
A valid inbound X-Request-ID or X-Correlation-ID is preserved when it is
1-128 characters matching ^[A-Za-z0-9._:-]+$. A missing or invalid request ID
is replaced with a generated UUID. A missing or invalid correlation ID defaults
to the effective request ID; invalid context values are not reflected or
logged, and they do not produce HTTP 400. Every application response exposes
the effective IDs in X-Request-ID and X-Correlation-ID. Both values live in
an explicit plain application context, and HTTP causationId remains the
effective request ID. Authentication adds only the current user actor type and
ID; tokens, roles, names, and email are not copied into persistence context.
All Product, Warehouse, Stock, Goods Receipt, and Goods Issue mutation routes
accept an optional Idempotency-Key. Keys are case-sensitive, 8-128 characters,
and use the same safe character set. The raw value is never stored: SHA-256 is
used in the unique actor/operation scope. The normalized business command is
hashed with canonical-json-v1, after validation and normalization.
The first successful keyed execution returns Idempotency-Replayed: false.
The same actor, operation, key, and normalized command later receives the stored
contract-neutral result with Idempotency-Replayed: true, fresh context
headers, and the response body appropriate to the requested legacy or v1
prefix. A changed command returns 409 and current authentication/RBAC is
evaluated before every replay. Only committed 2xx results are stored. Failures
do not reserve a key.
Acquisition, the complete domain mutation, StockMovement writes, the response snapshot, and completion share one MongoDB transaction. MongoDB's unique scope index is the concurrency authority; there is no process-local or external lock, cleanup worker, or retry worker. Replay snapshots are limited to 1 MiB and are retained for seven days. MongoDB TTL deletion is asynchronous, so a record can remain authoritative slightly longer; the key becomes reusable after physical TTL removal.
Models define the MongoDB data structure using Mongoose schemas.
The config layer contains reusable configuration code, such as MongoDB and Swagger setup.
This structure keeps the project understandable and avoids unnecessary complexity.
Use .env.example as a template for local configuration:
cp .env.example .envOn Windows PowerShell:
Copy-Item .env.example .envExample values:
NODE_ENV=development
PORT=3000
MONGODB_URI=mongodb://localhost:27017/inventory_management?replicaSet=rs0&directConnection=true
JWT_ACCESS_SECRET=change_this_access_token_secret
JWT_ACCESS_EXPIRES_IN=15m
DB_CONNECT_RETRIES=2
DB_CONNECT_RETRY_DELAY_MS=1000
SWAGGER_PRODUCTION_URL=https://inventory-management-api-6zuo.onrender.com
ADMIN_NAME=Initial Admin
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=change_this_admin_passwordThe .env file is ignored by Git and should not be committed.
NODE_ENV accepts development, test, or production. Startup also
validates the port, MongoDB URI scheme, access-token duration, bounded database
retries, and optional Swagger URL. Production JWT secrets must contain at least
32 characters and cannot use the example, Docker, test, or obvious placeholder
values. Invalid configuration exits before a database connection is attempted
and does not log the rejected value.
ADMIN_NAME, ADMIN_EMAIL, and ADMIN_PASSWORD are used only by
npm run seed:admin; normal API startup does not require them. Production
secrets are injected by the deployment platform. See
docs/security.md for rotation and incident policy.
The authoritative dry-run-first Phase 1 migration runbook is:
docs/production-data-notes.md
It defines the required lifecycle/version -> idempotency -> audit/outbox -> API read-index order, blocking conditions, idempotent re-runs, verification, and rollback limits. Before using an existing production database, review:
- existing mixed-case product SKUs
- existing stock movement types
- existing admin users
- MongoDB unique indexes
- legacy refresh token data
- production secret rotation
Work Package 3 requires the controlled lifecycle/version migration before enforcement traffic is enabled. The command is dry-run by default:
npm run migrate:phase1-lifecycleAfter reviewing invalid-version counts, orphan Stock reports, legacy movement
counts, and duplicate (stockId, aggregateVersion) candidates, apply explicitly:
npm run migrate:phase1-lifecycle -- --applyThe script uses MONGODB_URI, never runs at application startup, does not
delete data, and does not invent historical quantities, versions, or snapshots.
It backfills only safely derivable direct movement references and creates the
partial unique movement-version index only after duplicate preflight succeeds.
See docs/production-data-notes.md for deployment and rollback guidance.
Audit/outbox indexes use a separate dry-run-first migration:
npm run migrate:phase1-audit-outbox
npm run migrate:phase1-audit-outbox -- --applyRun dry-run against the intended database, inspect both collection/index plans and duplicate counts, apply explicitly, rerun dry-run, verify compatible indexes, then deploy and smoke-test runtime code. Its production execution was not independently established in this WP8 session. It creates no historical events: Audit and Outbox history begins at cutover. Pending OutboxEvents accumulate until a future delivery worker exists, and Audit/delivered-Outbox retention is deferred.
WP7 read indexes use a separate dry-run-first, no-drop migration. Production
startup uses autoIndex: false. Before any index creation, the migration
performs two separate checks: it verifies the existing Product SKU, Warehouse
code, Stock Product/Warehouse, and StockMovement aggregate-version prerequisite
integrity indexes; then it classifies the WP7 bounded-read indexes. It also
inspects Product, Warehouse, Stock, and StockMovement collection existence and
createdAt validity:
npm run migrate:phase1-api-read-indexes
npm run migrate:phase1-api-read-indexes -- --applyMissing required collections and missing or incompatible prerequisite indexes are blocking in dry-run and apply. Apply creates no WP7 index while any preflight blocker exists. The migration accepts semantically equivalent alternate names, but never creates, drops, rebuilds, renames, or repairs a prerequisite index and never creates an empty missing collection. Operators must investigate blockers and use the owning historical migration or an explicitly approved remediation plan. With a clean preflight, apply creates only absent compatible WP7 read indexes and never modifies documents or introduces TTL indexes. The lifecycle and API read-index migrations were externally reported as applied and verified before WP8; this session did not connect to or modify the production database.
These checks are operational deployment tasks and are not executed automatically by the application.
Install dependencies:
npm installCreate a local environment file:
cp .env.example .envCreate the initial admin user:
npm run seed:adminStart the development server:
npm run devThe server should start on:
http://localhost:3000
Run the automated test suite:
npm testBuild and start the API together with MongoDB:
docker compose up --buildCompose is intentionally the local-development runtime and overrides
NODE_ENV=development, so its documented JWT/admin placeholders are never
accepted as production credentials. The same image defaults to
NODE_ENV=production, contains production dependencies only, and runs as the
non-root node user. Render also runs in production mode with platform-injected
secrets.
Compose runs MongoDB 8 as a single-node replica set named rs0. Its health
check initializes the replica set when needed and waits for the node to become
primary before starting the API. The API container connects through:
mongodb://mongo:27017/inventory_management?replicaSet=rs0
When the application runs directly on the host against the Compose MongoDB, use:
mongodb://localhost:27017/inventory_management?replicaSet=rs0&directConnection=true
The existing mongo_data volume remains mounted and is not deleted by this
configuration. Do not use docker compose down -v when development data must
be retained.
The API will be available at:
http://localhost:3000
Health check endpoint:
http://localhost:3000/health
Swagger documentation:
http://localhost:3000/api-docs
To create the initial admin user inside the Docker Compose setup, run the seed service:
docker compose --profile seed run --rm seed-adminStop the containers:
docker compose downThe seed step is intentionally separated from normal application startup. Run it only when an initial administrator is required, then treat the bootstrap password as a temporary operational credential.
If the admin user has not been created yet, run the seed command with the correct environment variables:
npm run seed:adminThe seed step is not intended to run automatically on every application startup.
The project includes automated API, integration and configuration tests with Jest, Supertest and a transaction-capable MongoMemoryReplSet.
The tests cover:
- root API endpoint
- authentication workflows
- refresh token rotation
- logout token revocation
- login rate limiting
- rejection of public registration
- admin-only user creation
- role-based access control
- product API
- warehouse API
- stock API
- stock movement read routes
- goods receipt workflow
- goods issue workflow
- bulk operations
- request validation
- inactive stock rejection in inventory workflows
- inactive/archived Product and inactive Warehouse enforcement
- Product archive, reference preservation, lifecycle metadata and optimistic conflicts
- Stock lifecycle guard propagation and lifecycle-versus-inventory races
- explicit aggregate versions, transaction retry behavior and sequential bulk versions
- immutable StockMovement snapshots and historical direct references
- migration dry-run, apply, rerun, orphan, duplicate-index and connection-close behavior
- concurrent goods issue scenarios
- production error handling behavior
- MongoDB connection retry behavior
- required server environment variables
- public Swagger UI availability
- OpenAPI specification validation
- Docker, Docker Compose, Render and CI configuration checks
- disabled manual stock movement creation
Run tests:
npm testRelease verification commands:
npm ci
npm audit --omit=dev --audit-level=high
npm run verify:security
npm run verify:syntax
npm run validate:openapi
docker compose config --quiet
npm run verify:dockerverify:docker uses an isolated Compose project and temporary local ports,
checks liveness/readiness, container health, and the non-root runtime user, then
removes its containers and volumes even after failure.
Interactive API documentation is available through Swagger UI.
Local Swagger UI:
http://localhost:3000/api-docs
Production Swagger UI:
https://inventory-management-api-6zuo.onrender.com/api-docs
inventory-management-api/
|-- .github/workflows/ci.yml
|-- scripts/
| |-- migrations/
| | |-- phase1LifecycleVersion.js
| | |-- phase1IdempotencyIndexes.js
| | |-- phase1AuditOutboxIndexes.js
| | `-- phase1ApiReadIndexes.js
| |-- seedAdmin.js
| |-- validateOpenApi.js
| |-- verifyDocker.js
| |-- verifyJavaScriptSyntax.js
| `-- verifyRepositorySecurity.js
|-- src/
| |-- app.js
| |-- server.js
| |-- config/
| | |-- environment.js
| | |-- database.js
| | |-- logger.js
| | `-- swagger.js
| |-- controllers/
| |-- errors/
| |-- http/
| |-- middleware/
| |-- models/
| |-- routes/
| |-- runtime/
| |-- services/
| |-- utils/
| `-- validators/
|-- tests/
|-- docs/
| |-- Swagger-UI.png
| |-- architecture.md
| |-- phase1-audit.md
| |-- production-data-notes.md
| `-- security.md
|-- .env.example
|-- .dockerignore
|-- .gitignore
|-- Dockerfile
|-- docker-compose.yml
|-- package.json
|-- package-lock.json
|-- README.md
`-- render.yaml
Phase 1 implements the Inventory Core domains, transaction-safe lifecycle and
inventory mutations, optimistic versions, referential guards, request context,
idempotency, audit/outbox persistence, operational lifecycle/logging, canonical
/api/v1 plus bounded legacy compatibility, cursor reads/index migrations,
central environment validation, public validated Swagger, security/release CI
gates, and an isolated Docker runtime regression check.
The detailed evidence and limitations are recorded in
docs/phase1-audit.md. The baseline deployment was
externally reported live before WP8; the current unstaged WP8 changes have not
been committed, pushed, deployed, or production-smoke tested.
Phase 1 intentionally has no Outbox delivery worker, webhook delivery, n8n or AI integration, external message broker, Orders or Suppliers domain, machine-to-machine/service-to-service authentication, frontend, microservice decomposition, AWS or multi-region deployment, managed secret platform, distributed tracing/SIEM integration, or advanced load-testing claim.
Other current limits are the process-local login limiter, a single-instance Render demo topology, public Swagger by design, and Outbox records that remain pending until a separately approved delivery design exists.
Inventory and warehouse management are common real-world business problems. Companies need systems that can manage products, warehouses, stock levels, goods receipts, goods issues and movement history.
This project demonstrates backend skills that are relevant for roles such as:
- Backend Developer
- API Developer
- Integration Developer
- Software Developer
The project is focused on backend logic and API design. It shows how backend APIs can model real business rules instead of only simple CRUD operations.
The project follows a business-first approach.
Instead of updating inventory quantities directly, stock changes are handled through goods receipt and goods issue workflows. Each workflow updates the current stock quantity and creates stock movement history.
Some design choices are intentional:
- no public registration
- no automatic admin creation on application startup
- initial admin creation is an explicit setup step
- no direct stock quantity updates
- no manual stock movement creation
- stock movement history is generated by inventory workflows
- goods receipt and goods issue reject inactive stock records
- Product archive replaces supported runtime hard delete and preserves historical references
- inventory workflows require active Product, Warehouse, Stock, and synchronized lifecycle guards
- explicit
versionfields are domain revisions;__vremains Mongoose-internal - optional
expectedVersionis transitional and omission still permits last-write-wins - goods issue uses conditional stock updates to reduce normal overselling risk
- Inventory Core mutations use MongoDB transactions and support optional seven-day idempotent replay; callers that omit the header retain legacy behavior
- login rate limiting is process-local and suitable for this single-instance demo setup
- Swagger is public for portfolio/demo visibility, while protected endpoints still require authentication
- production data compatibility notes are documented separately in
docs/production-data-notes.md
ISC
This project is built as a backend portfolio project to demonstrate API design, authentication, RBAC, inventory workflows and integration-ready bulk operations.
