This project is a simple, extendable ecommerce backend built with NestJS, running on Node.js, with all source files written in TypeScript. TypeORM is the ORM used to handle the interactions with the database which is SQLite.
The primary goal is to implement core ecommerce functionalities such as order creation, updating status, updating shipping, and cancelling an order while keeping the system modular and easy to extend.
The project currently implements one main module: the Order Module. This receives two injected services, the Order Service for the interaction of the database for orders and the Inventory service, to receive product information at runtime.
The Order Controller provides endpoints to:
- Create new orders
- Update orders with shipping information
- Update order status
- Delete order
The controller interacts with an Order Service, which handles all database interactions related to order data.
| Method | Endpoint | Description |
|---|---|---|
| POST | /orders |
Create a new order |
| PATCH | /orders/:id/tracking |
Update tracking info for an order |
| PATCH | /orders/:id/status |
Update the status of an order |
| PATCH | /orders/:id/cancel |
Cancel an order, does not delete anything, but removes the shipping information and marks the order as cancelled |
| GET | /orders/:id |
Retrieve a specific order |
| GET | /orders |
Retrieve all orders (unauthenticated) |
A custom middleware extracts the "authenticated" user from the request. This is achieved by:
- Reading the
user_idfrom the request headers - Fetching the user from a mocked static array (User Module)
This middleware simplifies authenticated user retrieval across all endpoints, reducing duplication and making user data easily accessible for request handling. When a real authentication framework is added it should only need to be updated in one place.
In the future, a production-ready authentication framework (e.g., OAuth2, JWT, or Cognito) should replace the current mock authentication.
Performance can be improved by caching session or token data using services like DynamoDB or MemoryDB, with TTLs (time-to-live) for automatic session expiration and cleanup.
A Guard is applied to all order-related routes involving a specific :id parameter. Its responsibilities include:
- Verify that the order belongs to the authenticated user
- Respond with
Unauthorizedif:- The order does not exist
- The user does not own the order
This enforces basic security and ownership validation at the route level. This ensures each developer does not need to do their own verifications.
The current implementation uses SQLite as the local development datastore. It is used to mimic the use of a larger relational database such as Postgres or MySQL. TypeORM synchronizes the schemas whenever the service is started based on the entity models. In the future, a more robust migration strategy would be better.
The SQLite database will be created at project root directory /db
There are three relational tables designed for basic order management.
The orders table contains the general information of the order and has relations to related tables regarding the order.
An order has a one-to-one relationship with a shipment. A future improvement could allow orders to be split into multiple shipments based on the geolocation of products at the time of order or weight/size.
An order can have many order products, each representing an individual product for the given order.
| Column | Type | Attributes |
|---|---|---|
| id | number | Primary Key, Auto-generated |
| userId | number | Owner of the order (would be one-to-one with user table if it existed) |
| orderDate | Date | Date the order was created |
| shipment | OrderShipment (relation) | One-to-one (nullable) |
| orderProduct | OrderProduct[] (relation) | One-to-many |
| status | string | Current status of the order |
An order product maps one product to an order; there can be many order products for an order.
The price field is stored here since product prices can fluctuate over time, ensuring the price at the time of purchase is recorded for billing purposes.
Currently, there is no relationship between an order product and an order shipment; this is stored at the order level. If features are added to ship products separately, a many-to-one relationship could be added between order products and order shipments.
| Column | Type | Attributes |
|---|---|---|
| id | number | Primary Key, Auto-generated |
| order | Order (relation) | Many-to-one relation to Order |
| productId | number | ID of the product being ordered (would be one-to-one with product table if it existed) |
| price | number | Price of the product at time of purchase |
| quantity | number | Quantity of the product ordered |
An order shipment contains the tracking information for the given order, namely the shipping provider and tracking number.
| Column | Type | Attributes |
|---|---|---|
| id | number | Primary Key, Auto-generated |
| order | Order (relation) | One-to-one relation to Order |
| shippingProvider | string | Name of the shipping provider |
| trackingNumber | string | Tracking number for the shipment |
While NoSQL could technically be used for a small-scale implementation, relational databases are a better fit here due to:
- Complex access patterns (e.g., joins between users, orders, and products)
- Future features that would complicate NoSQL schema design and querying (ex: showing customers all their orders of specified times, querying orders across multiple columns, data analytics on customer order information)
There are many ways to implement this system depending on the use cases being targeted. For an initial launch of the service, using a low cost and low complexity approach would serve best to be able to monitor the usage and customer patterns on the system. After hitting some initial bottlenecks such as compute concurrency, a more highly available and scalable approach would be better with knowledge of patters from the initial approach.
Note: All these services described have strong support for L2 constructs in CDK and could easily be modeled into the diagrams provided below.
For a low-cost, low-complexity option, services that are pay-per-usage and have minimal idle cost are preferred.
- Compute: Lambda
- Database: Aurora Serverless
Lambda is fronted by API Gateway to serve REST endpoints. Lambda can handle up to 1,000 concurrent executions by default, though this can be adjusted with requests to AWS. Aurora Serverless can automatically pause and resume based on usage, allowing significant cost savings during idle periods (still pay for storage).
Note: DynamoDB could be an alternative, but would require major access pattern adjustments.
For highly available, high-traffic scenarios:
- Compute: ECS (Fargate) or EC2 instances
- Database: RDS or Aurora (with Multi-AZ or Multi-Region replication)
- Load Balancing: Route 53 ➔ ALB ➔ NLB (for private networking)
ECS Fargate tasks can quickly scale with traffic.
ALB handles public traffic, while NLB enables AWS PrivateLink for secure and fast, internal service to service communication.
Data Resiliency can be improved with:
- Automatic snapshots
- Multi-AZ deployments
- With multi-az there can be performance hits for read as this is a synchronous process
- Read replicas
- No performance hit as this is an asynchronous process
Order Processing Resiliency:
Switch from synchronous to asynchronous order fulfillment:
- Record order immediately and return success to customer that order is received
- Place message onto a queue (e.g., SQS)
- Backend services pick up orders and process asynchronously
- Notify users via email upon fulfillment and shipping
- Need to ensure orders are not duplicated (idempotent service) as SQS has at least once delivery
To support regionalized stores:
- Extend database schemas to store the marketplace region
- Route users via Route 53 based on geographic location, or allow users to select their marketplace manually
- Separate infrastructure per region if scaling internationally
- Look into usage of global tables such as Aurora DSQL or pull RDS instances out of application VPC into its own account and VPC. This would separate the storage layer from the business layer, and ensure each application is accessing the database in its own private cloud rather than sharing with other applications.
- Investigate pros and cons of multi tenant models versus a global store which the application code handles the separate marketplaces.
- Products are global, but have marketplace specific representations to handle price differences, language differences, model differences.
The server will start on http://localhost:3000.
You can interact with the API using any REST client (e.g., Postman, Insomnia) or standard curl commands.
# Install dependencies
npm install# Development mode
npm run start
# Watch mode (auto-reload on changes)
npm run start:dev
# Production mode
npm run start:prod
Create Order
curl --location '127.0.0.1:3000/orders' \
--header 'user_id: 1' \
--header 'Content-Type: application/json' \
--data '{
"products": [
{
"id": 9,
"quantity": 7
},
{
"id": 1,
"quantity": 1
}
]
}'Get All Orders
curl --location '127.0.0.1:3000/orders' \
--header 'user_id: 1'Update Order Tracking
curl --location --request PATCH '127.0.0.1:3000/orders/1/tracking' \
--header 'user_id: 1' \
--header 'Content-Type: application/json' \
--data '{
"shippingProvider": "USPS",
"trackingNumber": "23987239825"
}'
# Unauthorized
curl --location --request PATCH '127.0.0.1:3000/orders/1/tracking' \
--header 'user_id: 999' \
--header 'Content-Type: application/json' \
--data '{
"shippingProvider": "USPS",
"trackingNumber": "23987239825"
}'Cancel Order
curl --location --request PATCH '127.0.0.1:3000/orders/1/cancel' \
--header 'user_id: 1'

