This exercise is designed to demonstrate and display proficiency in Node.js and RESTful API design.
The tasks range from straightforward to intermediate and potentially challenging features.
The service must manage and persist a simple domain of three entities: Users, Organizations, and Orders.
- User:
id,firstName,lastName,email,organizationId,dateCreated - Organization:
id,name,industry,dateFounded - Order:
id,orderDate,totalAmount,userId,organizationId
The
userIdandorganizationIdvalues must reference valid records.
Each entity must support the following endpoints:
| Method | Route | Description |
|---|---|---|
| GET | /api/[entity] |
Returns all items (paginated) |
| GET | /api/[entity]/{id} |
Returns a single item by ID |
| POST | /api/[entity] |
Creates a new item |
| PUT | /api/[entity]/{id} |
Updates an existing item |
| DELETE | /api/[entity]/{id} |
Deletes an item |
GET /api/orders/{id}— returns the order along with the associated user and organization.POST /api/orders/bulk— creates multiple orders in a single request.
Creates multiple orders at once with validation:
| Rule | Description |
|---|---|
orders array |
Must contain at least 1 order |
organizationId |
All orders must have the same organizationId |
| Total sum | Sum of all totalAmount values must not exceed MAX_BULK_ORDER_TOTAL (default: 100,000) |
Request body:
{
"orders": [
{ "totalAmount": 100, "userId": "uuid", "organizationId": "uuid" },
{ "totalAmount": 200, "userId": "uuid", "organizationId": "uuid" }
]
}Note:
userIdandorganizationIdare optional. If omitted, they default to the authenticated user's values.
Environment variable:
MAX_BULK_ORDER_TOTAL— Maximum allowed sum of all order amounts (default: 100,000)
POST and PUT requests must be validated and respond with appropriate HTTP status codes.
| Rule | Description |
|---|---|
User firstName, lastName |
Must not be null or whitespace |
Organization name |
Must not be null or whitespace |
Order totalAmount |
Must be greater than 0 |
| All date fields | Must occur before the current timestamp |
- Swagger/OpenAPI must be available at:
GET /swagger - Health probes:
GET /health— livenessGET /readiness— readiness (check DB connection and cache readiness)
Provide a simple seed script that creates:
- 2 organizations
- 10 users
- 20 orders (with valid past dates)
This will help with testing pagination, relationships, and validation rules.
- Tech Stack: Node.js, Typescript, Express, MySQL with Sequelize ORM
- Separate concerns between:
- Controllers
- Business logic (services)
- Data access (repositories)
- Domain entities must not be directly exposed in HTTP responses.
Use DTOs or mapping functions. - Logging:
- Database state changes →
infolevel - HTTP headers →
debuglevel
- Database state changes →
- Implement unit tests for business logic.
- Deploy the service via Docker, including dependencies
- Handle unhandled exceptions gracefully — return a structured JSON error message, not the developer exception page.
- Client-side caching headers
- User and Organization responses: cacheable for 10 minutes
- Order responses: use ETag headers (
304 Not Modifiedwhen valid)
- Server-side caching
- Cache GET responses in memory (e.g., using
lru-cache) - TTL: 10 minutes
- Invalidate cached entries when related data changes
- Cache GET responses in memory (e.g., using
- Rate limiting
- Limit API access per organization to 30 requests per minute
- Authentication
- Implement JWT/OAuth2 authentication
- All routes require authorization except
/health,/readiness, and/swagger
- Secure configuration
- No hardcoded credentials in the source code (implement industry recognized security standards)
The final repository must include:
- Complete source code
README.md(this file)docker-compose.ymlDockerfile.env.example- Swagger documentation setup
- Unit tests
- How to run the app locally
- How to run it with Docker
- How to access the Swagger UI
- How to run the test suite
- A short note on key design decisions (ORM, error handling, caching, etc.)
- Prerequisites: Ensure you have Node.js (v22+) and MySQL 8 installed.
- Install Dependencies:
npm install
- Environment Setup:
Copy
.env.exampleto.envand update database credentials.cp .env.example .env
- Database Setup:
Run migrations and seed data:
npm run db:migrate npm run db:seed
- Start the Server:
npm run dev
Run the following command to start the application and the database:
docker compose upTo rebuild the image (e.g. after installing new dependencies):
docker compose down && docker compose build --no-cache && docker compose up-
Start the app with the local compose file:
docker compose up
-
Open Chrome and navigate to
chrome://inspect -
Click "Configure" and ensure
localhost:9229is in the list -
Your Node.js app will appear under Remote Target — click inspect to open DevTools
You can now set breakpoints, inspect variables, and step through code.
Once the application is running, you can access the interactive API documentation at:
To run the unit tests:
npm testTo run tests with coverage report:
npm run test:coverageE2E tests require a running MySQL database. The easiest way is to use Docker:
-
Start the database and application:
docker compose up -d
-
Run migration and db seeding
npm run db:migrate
npm run db:seed
-
Run E2E tests:
npm run test:e2e
The E2E tests will run against the containerized application and database, testing the full request/response flow including authentication, CRUD operations, and pagination.
- Architecture: Implemented a layered architecture (Controllers, Services, Data Access) with Dependency Injection to ensure separation of concerns and testability.
- Validation: Used Zod for strict runtime request validation and environment variable verification.
- Database: Chosen Sequelize as the ORM for its easy integration with MySQL.
- Error Handling: Centralized error handling middleware that captures exceptions and returns structured JSON responses, ensuring no sensitive stack traces leak in production.
- Testing: Utilized Vitest for a fast, modern testing experience.
- Docker: Configured a multi-environment Docker setup (
docker-compose.ymlvsdocker-compose.local.yml) to support both production-like execution and local development with hot-reloading.
The API implements a multi-layer caching strategy:
- Client-side cache (Cache-Control) - Browser caches responses, no request made
- Server-side LRU cache - In-memory cache, no database query
- ETag validation - Returns 304 if content unchanged
┌─────────────────────┐
│ GET /api/users │
└──────────┬──────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ CLIENT (Browser) │
│ │
│ ┌─────────────────────────────┐ │
│ │ Cache-Control still valid? │ │
│ │ (max-age not expired) │ │
│ └──────────────┬──────────────┘ │
│ │ │ │
│ YES NO │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ Use cached │ │ Send request to server │ │
│ │ response │ │ + If-None-Match: <etag> │ │
│ │ │ └─────────────┬────────────┘ │
│ │ (instant!) │ │ │
│ └──────────────┘ │ │
│ │ │
└────────────────────────────────────────┼─────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ SERVER │
│ │
│ ┌─────────────────────────────┐ │
│ │ Check LRU Cache │ │
│ └──────────────┬──────────────┘ │
│ │ │ │
│ HIT MISS │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ ETag matches │ │ Query DB │ │
│ │ request? │ │ + cache it │ │
│ └───────┬──────┘ └───────┬──────┘ │
│ │ │ │ │
│ YES NO ▼ │
│ │ │ ┌──────────────┐ │
│ │ │ │ ETag matches │ │
│ │ │ │ request? │ │
│ │ │ └───────┬──────┘ │
│ │ │ │ │ │
│ │ │ YES NO │
│ ▼ ▼ ▼ ▼ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ 304 │ │ 200 │ │ 304 │ │ 200 │ │
│ │ │ │+body│ │ │ │+body│ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
│ X-Cache X-Cache X-Cache X-Cache │
│ HIT HIT MISS MISS │
│ │
└──────────────────────────────────────────────────────────────┘
Response headers:
Cache-Control: private, max-age=600- Users/Organizations (client caches 10 min)Cache-Control: no-cache- Orders (must revalidate with ETag)ETag- Hash of response body for validationX-Cache: HIT/MISS- Server-side cache status
Cache invalidation:
- Server cache entries invalidated on mutations (POST/PUT/DELETE)
- TTL: 10 minutes