Skip to main content

Food Delivery Service API — Technical Specification

1. Overview

A RESTful API for a food delivery service where customers can order meals from restaurants. The system supports three roles — Customer, Restaurant Owner, and Administrator — each with distinct permission levels. The API handles authentication, authorization, CRUD operations, order lifecycle management, coupon discounts, tipping, and order history.


2. Technology Stack

LayerTechnologyVersionRationale
RuntimeNode.js20 LTSStable, excellent TypeScript support, long-term support
LanguageTypeScript5.xType safety, better DX, catches bugs at compile time
FrameworkExpress.js4.xIndustry standard, minimal, well-documented, huge middleware ecosystem
DatabasePostgreSQL16ACID compliance, relational integrity for orders/meals, JSON support, robust
ORMPrisma7.7.0Type-safe queries auto-generated from schema, excellent migration system, great DX. Uses driver adapters for better connection management.
AuthenticationJWT (jsonwebtoken)9.xStateless auth, perfect for REST APIs, well-understood pattern
Password Hashingbcrypt5.xIndustry standard for password hashing, adaptive cost factor
ValidationZod3.xTypeScript-first schema validation, excellent inference, composable
TestingJest + Supertest29.x / 7.xJest: full-featured test runner; Supertest: HTTP assertion library for integration tests
ContainerizationDocker + Docker ComposelatestConsistent dev environment, easy PostgreSQL setup, reproducible builds
API DocumentationSwagger/OpenAPI via swagger-jsdoc + swagger-ui-express-Auto-generated interactive docs, doubles as Postman-importable spec
LoggingPino8.xFast structured JSON logging, low overhead
Environment Configdotenv17.xSimple .env.local / .env.test file loading
HTTP Client (tests)Supertest (NOT axios)7.xDirect HTTP testing without axios, tests Express app in-process
UUID Generationuuidv7 (npm)1.xUUIDv7 is time-ordered — dramatically better B-tree index performance vs UUIDv4
In-Memory Cachenode-cache5.xLightweight TTL cache for auth user lookups; avoids Redis dependency
Rate Limitingexpress-rate-limit7.xProtects auth endpoints, simple setup
CORScors2.xCross-origin support for Postman/frontend clients
Helmethelmet7.xSecurity headers out of the box
Postgres Driverpg8.xRobust PostgreSQL client for Node.js, used as the driver for Prisma
Prisma Adapter@prisma/adapter-pg7.xOfficial adapter for Prisma to use pg as the database driver

Why NOT these alternatives?

RejectedReason
AxiosExplicitly excluded per requirement (compromised npm module)
TypeORMPrisma offers better type safety, migration UX, and developer experience
SequelizeDated API, weaker TypeScript support compared to Prisma
NestJSOver-engineered for a focused take-home; Express is leaner and more transparent
MongoDBRelational data (orders → meals → restaurants) benefits from PostgreSQL's referential integrity
Passport.jsAdds abstraction we don't need; custom JWT middleware is simpler and more transparent

3. Project Structure

├── docker-compose.yml
├── Dockerfile
├── .env.example
├── .env.local
├── .env.test
├── package.json
├── tsconfig.json
├── jest.config.ts
├── prisma/
│ ├── schema.prisma # Database schema
│ ├── migrations/ # Auto-generated migrations
│ └── seed.ts # Seed built-in admin + test data
├── src/
│ ├── app.ts # Express app setup (middleware, routes)
│ ├── server.ts # Server entry point (listen)
│ ├── config/
│ │ └── index.ts # Environment config with validation
│ ├── middleware/
│ │ ├── authenticate.ts # JWT verification middleware
│ │ ├── authorize.ts # Role-based permission middleware
│ │ ├── validate.ts # Zod schema validation middleware
│ │ ├── errorHandler.ts # Global error handler
│ │ └── rateLimiter.ts # Rate limiting for auth endpoints
│ ├── modules/
│ │ ├── auth/
│ │ │ ├── auth.controller.ts
│ │ │ ├── auth.service.ts
│ │ │ ├── auth.routes.ts
│ │ │ ├── auth.schema.ts # Zod validation schemas
│ │ │ └── auth.test.ts
│ │ ├── users/
│ │ │ ├── users.controller.ts
│ │ │ ├── users.service.ts
│ │ │ ├── users.routes.ts
│ │ │ ├── users.schema.ts
│ │ │ └── users.test.ts
│ │ ├── restaurants/
│ │ │ ├── restaurants.controller.ts
│ │ │ ├── restaurants.service.ts
│ │ │ ├── restaurants.routes.ts
│ │ │ ├── restaurants.schema.ts
│ │ │ └── restaurants.test.ts
│ │ ├── meals/
│ │ │ ├── meals.controller.ts
│ │ │ ├── meals.service.ts
│ │ │ ├── meals.routes.ts
│ │ │ ├── meals.schema.ts
│ │ │ └── meals.test.ts
│ │ ├── orders/
│ │ │ ├── orders.controller.ts
│ │ │ ├── orders.service.ts
│ │ │ ├── orders.routes.ts
│ │ │ ├── orders.schema.ts
│ │ │ └── orders.test.ts
│ │ └── coupons/
│ │ ├── coupons.controller.ts
│ │ ├── coupons.service.ts
│ │ ├── coupons.routes.ts
│ │ ├── coupons.schema.ts
│ │ └── coupons.test.ts
│ ├── utils/
│ │ ├── errors.ts # Custom error classes (AppError, NotFoundError, etc.)
│ │ ├── prisma.ts # Prisma client singleton
│ │ └── response.ts # Standardized API response helpers
│ └── types/
│ └── index.ts # Shared TypeScript types/interfaces
├── docs/
│ ├── tech-specs.md # This document
│ ├── assumptions.md # Assumptions document
│ └── key-decisions.md # Key decisions document
└── tests/
├── setup.ts # Global test setup (DB reset, seed admin)
├── helpers.ts # Test utilities (login, create fixtures)
└── integration/ # End-to-end API tests
├── auth.test.ts
├── users.test.ts
├── restaurants.test.ts
├── meals.test.ts
├── orders.test.ts
└── coupons.test.ts

4. Database Schema (Prisma)

4.1 Entity Relationship Diagram

4.2 Enums

enum Role {
CUSTOMER = 'CUSTOMER'
RESTAURANT_OWNER = 'RESTAURANT_OWNER'
ADMIN = 'ADMIN'
}

enum OrderStatus {
PLACED = 'PLACED'
CANCELED = 'CANCELED'
PROCESSING = 'PROCESSING'
IN_ROUTE = 'IN_ROUTE'
DELIVERED = 'DELIVERED'
RECEIVED = 'RECEIVED'
}

4.3 Key Schema Details

  • User.isBuiltInAdmin: true only for the seeded admin; prevents deletion.
  • Meal.isAvailable: Soft toggle for restaurant owners to hide meals without deleting.
  • Meal.isBlocked / Restaurant.isBlocked / User.isBlocked: Admin-level blocking support.
  • OrderItem.unitPrice: Snapshot of the meal price at order time (prices may change later).
  • Order.subtotal: Sum of all OrderItem.itemTotal values before discount/tip.
  • Order.discountAmount: Calculated from coupon percentage × subtotal.
  • Order.totalAmount: subtotal - discountAmount + tipAmount.
  • Coupon.maxUsageTotal: Maximum number of times this coupon can be used across all customers. null = unlimited.
  • Coupon.maxUsagePerCustomer: Maximum uses per individual customer. null = unlimited.
  • Coupon.currentUsageTotal: Counter tracking total redemptions (incremented atomically in a transaction).
  • CouponUsage: Join table tracking which customer used which coupon on which order. Used for per-customer limit enforcement.
  • UUIDv7: All primary keys use UUIDv7 (time-ordered). This ensures sequential inserts into B-tree indexes, providing ~2-5x better insert performance compared to random UUIDv4.

5. API Endpoints

5.1 Authentication

MethodEndpointDescriptionAuth
POST/api/v1/auth/registerRegister a new user (Customer or Restaurant Owner)Public
POST/api/v1/auth/loginLogin and receive access + refresh tokensPublic
POST/api/v1/auth/refreshExchange refresh token for new access tokenPublic (refresh token in body)
POST/api/v1/auth/logoutInvalidate refresh tokenAuthenticated
GET/api/v1/auth/meGet current user profileAuthenticated
PATCH/api/v1/auth/meUpdate own profile (name, password)Authenticated

5.2 Users (Admin only)

MethodEndpointDescriptionAuth
GET/api/v1/usersList all users (paginated, filterable)Admin
GET/api/v1/users/:idGet user by IDAdmin
POST/api/v1/usersCreate a user (any role)Admin
PATCH/api/v1/users/:idUpdate user infoAdmin
DELETE/api/v1/users/:idDelete user (not built-in admin)Admin
PATCH/api/v1/users/:id/blockBlock/unblock a userAdmin

5.3 Restaurants

MethodEndpointDescriptionAuth
GET/api/v1/restaurantsList all restaurants (paginated)Authenticated
GET/api/v1/restaurants/:idGet restaurant detailsAuthenticated
POST/api/v1/restaurantsCreate restaurantOwner, Admin
PATCH/api/v1/restaurants/:idUpdate restaurantOwner (own), Admin
DELETE/api/v1/restaurants/:idDelete restaurantOwner (own), Admin
PATCH/api/v1/restaurants/:id/blockBlock/unblock restaurantAdmin

5.4 Meals

MethodEndpointDescriptionAuth
GET/api/v1/restaurants/:restaurantId/mealsList meals for a restaurantAuthenticated
GET/api/v1/restaurants/:restaurantId/meals/:idGet meal detailsAuthenticated
POST/api/v1/restaurants/:restaurantId/mealsCreate mealOwner (own restaurant), Admin
PATCH/api/v1/restaurants/:restaurantId/meals/:idUpdate mealOwner (own restaurant), Admin
DELETE/api/v1/restaurants/:restaurantId/meals/:idDelete mealOwner (own restaurant), Admin

5.5 Orders

MethodEndpointDescriptionAuth
POST/api/v1/ordersPlace a new orderCustomer
GET/api/v1/ordersList orders (own orders for customer; restaurant orders for owner; all for admin)Authenticated
GET/api/v1/orders/:idGet order details with status historyAuthenticated
PATCH/api/v1/orders/:id/statusUpdate order statusCustomer, Owner, Admin

5.6 Coupons

MethodEndpointDescriptionAuth
GET/api/v1/restaurants/:restaurantId/couponsList coupons for a restaurantAuthenticated
POST/api/v1/restaurants/:restaurantId/couponsCreate couponOwner (own restaurant), Admin
PATCH/api/v1/restaurants/:restaurantId/coupons/:idUpdate couponOwner (own restaurant), Admin
DELETE/api/v1/restaurants/:restaurantId/coupons/:idDelete couponOwner (own restaurant), Admin

6. Authentication & Authorization

6.1 Authentication Flow

┌──────────┐ POST /auth/login ┌──────────┐
│ Client │ ──────────────────────▶ │ Server │
│ (Postman) │ {email, password} │ │
│ │ │ Verify │
│ │ ◀────────────────────── │ Password │
│ │ {accessToken, │ + Issue │
│ │ user} │ JWT │
└──────────┘ └──────────┘

Subsequent requests: │
Authorization: Bearer <token> │

Middleware
verifies
JWT on every
protected route

6.2 JWT Token Design

Access Token (short-lived):

{
"sub": "user-uuid",
"email": "user@example.com",
"role": "CUSTOMER",
"type": "access",
"iat": 1712700000,
"exp": 1712700900
}

Refresh Token (long-lived):

{
"sub": "user-uuid",
"type": "refresh",
"jti": "unique-token-id",
"iat": 1712700000,
"exp": 1713304800
}
TokenTTLPurpose
Access Token15 minutesUsed in Authorization: Bearer <token> for API requests
Refresh Token7 daysUsed to obtain new access tokens without re-login
  • Access Secret: JWT_ACCESS_SECRET env var.
  • Refresh Secret: JWT_REFRESH_SECRET env var (separate secret for defense in depth).
  • Algorithm: HS256.
  • Refresh Token Storage: Stored in DB (RefreshToken table) with userId, jti, expiresAt, isRevoked. This allows server-side invalidation on logout or user block.
  • Rotation: On each /auth/refresh, the old refresh token is revoked and a new pair (access + refresh) is issued.

6.3 Authorization Middleware

// Usage in routes:
router.post('/restaurants',
authenticate, // Verifies JWT, attaches req.user
authorize('RESTAURANT_OWNER', 'ADMIN'), // Checks role
validate(createRestaurantSchema), // Validates body
controller.create
);
  • authenticate: Extracts and verifies JWT from Authorization: Bearer <token> header. Rejects blocked users.
  • authorize(...roles): Checks if req.user.role is in the allowed roles list.
  • validate(schema): Validates req.body / req.query / req.params against Zod schemas.

6.4 Ownership Checks

Beyond role checks, resource-level authorization ensures:

  • Restaurant Owners can only modify their own restaurants and meals.
  • Customers can only view/modify their own orders.
  • These checks happen in the service layer, not middleware, for cleaner separation.

7. Order Status State Machine

Valid Transitions Matrix

Current Status→ Canceled→ Processing→ In Route→ Delivered→ Received
PlacedCustomer, OwnerOwner
ProcessingOwnerOwner
In RouteOwner
DeliveredCustomer
Canceled
Received

Admin can perform any valid transition regardless of the role column above.


8. Request/Response Format

Standard Success Response

{
"success": true,
"data": { ... },
"meta": {
"page": 1,
"limit": 20,
"total": 100,
"totalPages": 5
}
}

Standard Error Response

{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [
{ "field": "email", "message": "Invalid email format" }
]
}
}

HTTP Status Codes Used

CodeUsage
200Successful GET, PATCH
201Successful POST (resource created)
204Successful DELETE
400Validation errors
401Missing/invalid authentication
403Insufficient permissions
404Resource not found
409Conflict (duplicate email, etc.)
422Business logic error (invalid status transition, etc.)
429Rate limit exceeded
500Internal server error

9. Pagination & Field Selection

All list (GET collection) endpoints support pagination, search, sorting, and field selection by default. Single-resource GET endpoints support field selection only.

9.1 Pagination Parameters

GET /api/v1/restaurants?page=2&limit=10&search=pizza&sortBy=name&sortOrder=asc
ParamDefaultDescription
page1Page number
limit20Items per page (max 100)
searchSearch by name (where applicable)
sortBycreatedAtSort field
sortOrderdescasc or desc

9.2 Field Selection (Sparse Fieldsets)

All GET endpoints support the fields query parameter to return only requested fields:

GET /api/v1/users?fields=id,email,name,role
GET /api/v1/restaurants/abc-123?fields=id,name,description
  • If fields is omitted, all fields are returned (default behavior).
  • Field names are validated against a whitelist per resource — requesting non-existent or sensitive fields (e.g., password) returns a 400 error.
  • Implementation: The fields query param is parsed into a Prisma select object. Since Prisma natively supports select, this is a lightweight addition.
  • Nested relations are NOT selectable via this param (use dedicated endpoints instead).
// Example: controller parses fields into Prisma select
const select = parseFieldSelection(req.query.fields, USER_SELECTABLE_FIELDS);
const users = await prisma.user.findMany({ select, ...pagination });

10. Testing Strategy

10.1 Test Categories

CategoryToolScope
Integration TestsJest + SupertestFull HTTP request → DB → response cycle
Unit TestsJestService-layer business logic (status transitions, calculations)

10.2 Test Database Strategy

  • A separate test database (food_delivery_test) spun up via Docker Compose.
  • Tests run migrations before execution.
  • Each test suite uses transactions (or truncation) to isolate test data.
  • Global setup seeds the built-in admin account.

10.3 Test Coverage Goals

ModuleKey Test Cases
AuthRegister, login, duplicate email, invalid credentials, blocked user
UsersAdmin CRUD, block/unblock, cannot delete built-in admin, role validation
RestaurantsOwner CRUD, customer read-only, admin CRUD, blocked restaurant
MealsOwner CRUD under own restaurant, price validation, admin override
OrdersPlace order, status transitions (all valid/invalid), tip, coupon discount, history
CouponsCreate, apply to order, expired coupon, invalid coupon

10.4 Postman Collection

  • A Postman collection (postman/food-delivery-api.postman_collection.json) will be exported.
  • Uses environment variables ({{baseUrl}}, {{accessToken}}).
  • Includes pre-request scripts to auto-login and set tokens.
  • Covers all endpoints with example requests and expected responses.

11. Docker Setup

docker-compose.yml

services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: food_delivery
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data

db-test:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: food_delivery_test
ports:
- "5433:5432"

app:
build: .
ports:
- "3000:3000"
depends_on:
- db
environment:
DATABASE_URL: postgresql://postgres:postgres@db:5432/food_delivery
volumes:
- .:/app
- /app/node_modules
command: npm run dev

volumes:
pgdata:

12. Environment Variables

# Server
PORT=3000
NODE_ENV=development

# Database
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/food_delivery

# JWT
JWT_ACCESS_SECRET=your-access-secret-key-minimum-32-chars
JWT_REFRESH_SECRET=your-refresh-secret-key-minimum-32-chars
JWT_ACCESS_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d

# Built-in Admin
ADMIN_EMAIL=admin@fooddelivery.com
ADMIN_PASSWORD=Admin@123456
ADMIN_NAME=System Admin

# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100

# Cache
USER_CACHE_TTL_SECONDS=60

13. Scripts (package.json)

{
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"db:migrate": "prisma migrate dev",
"db:migrate:deploy": "prisma migrate deploy",
"db:seed": "prisma db seed",
"db:reset": "prisma migrate reset --force",
"db:studio": "prisma studio",
"test": "jest --runInBand --forceExit",
"test:watch": "jest --watch --runInBand",
"test:coverage": "jest --coverage --runInBand --forceExit",
"lint": "eslint src/ tests/",
"format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'"
}
}

14. Error Handling Architecture

// Custom error hierarchy
class AppError extends Error {
constructor(
public statusCode: number,
public code: string,
message: string,
public details?: any[]
) { super(message); }
}

class NotFoundError extends AppError { /* 404 */ }
class ValidationError extends AppError { /* 400 */ }
class UnauthorizedError extends AppError { /* 401 */ }
class ForbiddenError extends AppError { /* 403 */ }
class ConflictError extends AppError { /* 409 */ }
class BusinessLogicError extends AppError { /* 422 */ }

Global error handler middleware catches all errors, logs them, and returns standardized responses. Prisma-specific errors (unique constraint, not found) are mapped to appropriate HTTP responses.


15. Security Considerations

ConcernMitigation
Password storagebcrypt with cost factor 12
SQL injectionPrisma parameterized queries (ORM prevents raw SQL injection)
XSSHelmet security headers, no HTML rendering
Rate limitingexpress-rate-limit on auth endpoints
Input validationZod schemas on all inputs
CORSConfigurable origins
SecretsEnvironment variables, never committed
Blocked usersJWT middleware checks isBlocked status (cached with 60s TTL)
Token revocationRefresh tokens stored in DB, revoked on logout/block
Admin protectionBuilt-in admin cannot be deleted (enforced in service layer)

16. Caching Strategy

We use node-cache (in-process, TTL-based) to avoid a DB round-trip on every authenticated request.

What is cached

Cache KeyTTLDataInvalidation
user:{id}60 seconds{ id, role, isBlocked }On user update/block (cache.del)

Why node-cache, not Redis?

Factornode-cacheRedis
InfrastructureNone (in-process)Separate server/container
Latency~0ms (memory)~0.5ms (network)
ScalabilitySingle-process onlyMulti-process/multi-server
Fit for take-home✅ Perfect❌ Over-engineered

For a single-process API (which this is), node-cache is the right tool. In production with horizontal scaling, we'd swap to Redis. The cache interface is abstracted behind a CacheService so this swap is a single-file change.

How it works in the auth middleware

// authenticate.ts
const cached = cache.get<UserCacheData>(`user:${decoded.sub}`);
if (cached) {
if (cached.isBlocked) throw new UnauthorizedError('Account is blocked');
req.user = { id: cached.id, role: cached.role, ...decoded };
return next();
}
// Cache miss → DB lookup → cache.set(`user:${id}`, data, TTL)

Worst case: A blocked user retains access for up to 60 seconds before the cache expires. This is an acceptable trade-off vs. hitting the DB on every single request.


17. Transaction Strategy

When we use Prisma transactions

OperationTransaction?Reason
Order creationprisma.$transaction()Creates Order + OrderItems + OrderStatusHistory + CouponUsage + increments coupon counter atomically. Partial creation would leave orphan records.
Order status changeprisma.$transaction()Updates Order.status + creates OrderStatusHistory entry. Both must succeed or neither.
User deletionprisma.$transaction()Deletes user + revokes all refresh tokens + cascades related data.
Restaurant deletionprisma.$transaction()Guards against active orders, then deletes restaurant + meals + coupons atomically.
Single-row CRUD❌ No transactionprisma.user.create(), prisma.meal.update() etc. are inherently atomic — Prisma wraps single operations in implicit transactions.
Read-only queries❌ No transactionNo mutation = no consistency risk.

Why explicit transactions for multi-table writes?

Without a transaction, a failure between steps leaves the database in an inconsistent state:

  • Order created but OrderItems missing → customer sees empty order.
  • Coupon usage counter incremented but order creation fails → coupon wasted.

Prisma's $transaction() uses PostgreSQL's BEGIN...COMMIT/ROLLBACK, ensuring atomicity.

// Example: Order creation transaction
const order = await prisma.$transaction(async (tx) => {
const newOrder = await tx.order.create({ data: orderData });
await tx.orderItem.createMany({ data: itemsData });
await tx.orderStatusHistory.create({ data: { orderId: newOrder.id, status: 'PLACED', ... } });
if (coupon) {
await tx.coupon.update({ where: { id: coupon.id }, data: { currentUsageTotal: { increment: 1 } } });
await tx.couponUsage.create({ data: { couponId: coupon.id, customerId, orderId: newOrder.id } });
}
return newOrder;
});

18. Admin API URL Design

Admin-only endpoints live under /api/v1/admin/... to provide a clear separation.

Endpoint PatternWho accesses it
/api/v1/auth/*Everyone (public + authenticated)
/api/v1/restaurants/*, /api/v1/orders/*Customers + Owners (role-checked in middleware)
/api/v1/admin/users/*Admin only
/api/v1/admin/restaurants/:id/blockAdmin only
/api/v1/admin/users/:id/blockAdmin only

Why prefix admin routes?

  • Self-documenting: Anyone reading the URL immediately knows it's an admin operation.
  • Security layering: A reverse proxy or API gateway can add extra protection (IP whitelisting, additional auth) to /admin/* routes without touching application code.
  • Postman organization: Admin routes group naturally in a dedicated folder.
  • Audit logging: Easy to add middleware that logs all /admin/* requests for compliance.

Non-admin operations on shared resources (e.g., owner updating their own restaurant) stay on the standard /api/v1/restaurants/:id routes with role checks in middleware.


19. Tax Calculation — Future Roadmap

Note: Tax calculation is not implemented in the current version. This section documents the planned approach for a future release.

Planned Schema Additions

model TaxConfig {
id String @id @default(uuid())
name String // e.g., "GST", "Sales Tax", "VAT"
rate Decimal @db.Decimal(5, 4) // e.g., 0.0825 for 8.25%
isInclusive Boolean @default(false) // true = tax included in meal price
region String? // e.g., "US-CA", "IN-MH" — nullable for flat-rate
restaurantId String? // nullable = global; set = restaurant-specific override
isActive Boolean @default(true)
createdAt DateTime @default(now())
}

// Order model additions:
model Order {
// ... existing fields ...
taxRate Decimal? @db.Decimal(5, 4)
taxAmount Decimal? @db.Decimal(10, 2)
// totalAmount becomes: subtotal - discountAmount + taxAmount + tipAmount
}

Calculation Flow

subtotal = Σ(quantity × unitPrice)
discountAmount = subtotal × (couponPercent / 100)
taxableAmount = subtotal - discountAmount
taxAmount = taxableAmount × taxRate
totalAmount = taxableAmount + taxAmount + tipAmount

Design Decisions (Future)

  • Tax rate is snapshotted into the Order at creation time (same pattern as price snapshotting).
  • Tax is applied after discount, before tip (standard practice).
  • isInclusive flag supports both tax-exclusive (US) and tax-inclusive (EU/India) models.
  • Restaurant-level overrides allow different tax rates per restaurant (useful for multi-state/multi-country).
  • Admin can CRUD TaxConfig entries.

20. Development Workflow

  1. docker compose up -d db db-test — Start PostgreSQL instances
  2. npm install — Install dependencies
  3. npx prisma migrate dev — Run migrations
  4. npx prisma db seed — Seed built-in admin
  5. npm run dev — Start development server
  6. npm test — Run all tests
  7. Import Postman collection for manual testing

API Testing

You can test the API using our Postman collection and environment: