Warming up the neural circuits...
By the end of this chapter you will:
When a senior developer tells you "the controller is calling the repository directly — that breaks layered architecture," you need to know what all those words mean.
Read through once. The goal is recognition — when you see a term in a code review, you know where to look it up.
Plain English: Each part of the code has one job and only talks to the part directly below it.
Why it matters: If your controller imports a Sequelize model directly, now you need a running database to test your HTTP logic. Split it out and you can test each layer in isolation.
// ❌ Anti-pattern: controller doing SQL directly
@Get(':id')
async findOne(@Param('id') id: string) {
return Order.findByPk(id); // SQL in the HTTP layer
}
// ✅ Correct: controller delegates to service
@Get(':id')
async findOne(@Param('id') id: string) {
return this.ordersService.findOne(id);
}Plain English: A self-contained area of the app that owns its own data and doesn't let others touch it directly.
In QuickBite: Users, Restaurants, Orders, Payments, and Deliveries are all separate bounded contexts. Each has its own module, models, and services.
If OrdersService needs to notify the restaurant, it doesn’t call Restaurant.update(...) directly. It calls RestaurantsService.notifyNewOrder(...). The day Restaurants moves to a separate , only that one call needs to change.
Plain English: Instead of a class creating its own dependencies, they are "injected" (handed) to it from outside.
A chef shouldn't go to the farm to get ingredients — someone hands them to the chef at the start of the shift. That's dependency injection. The chef (service) just declares "I need eggs and butter," and the kitchen manager (NestJS) provides them.
Without DI:
class OrdersService {
private restaurantsService = new RestaurantsService(); // creates its own — hard to test!
}With DI (NestJS handles this):
class OrdersService {
constructor(private restaurantsService: RestaurantsService
NestJS's DI system reads the @Module definitions and automatically wires everything together. You declare what you need; NestJS provides it.
Plain English: The shape of data that crosses a layer boundary — what the client sends and what the server returns.
Think of it as a form. When a client sends POST /orders, the body must match the CreateOrderDto . If any field is wrong, the request is rejected before it touches your business logic.
Never return a database model directly from an endpoint. It exposes every column — including ones you don't want the client to see (internal flags, soft-delete timestamps, etc.). Always map to an output DTO.
// ❌ Dangerous: leaks internal fields
return order; // returns internal flags, deleted_at, hashed tokens, everything
// ✅ Safe: only expose what you intend
return OrderResponse.from(order); // { id, status, totalAmount, createdAt }Plain English: When something happens, announce it. Let other parts of the system react independently.
Instead of:
// OrdersService knows too much
await this.emailService.sendConfirmation(order);
await this.restaurantService.notifyNewOrder(order);
await this.metricsService.increment('orders');Do:
// OrdersService just announces what happened
this.eventEmitter.emit('order.placed', new OrderPlacedEvent(order));
// Each listener handles their own job separatelyWhy it matters: As the system grows, OrdersService would otherwise need to import 15 other services. Events keep it clean.
Plain English: A versioned script that changes the database structure in a controlled, tracked way.
Think of migrations like Git commits for your database. Each one is numbered, applied once, and never edited after it runs.
Never edit a migration after it has run on any environment (dev, staging, prod). Write a new one instead.
Plain English: A rule the database itself enforces, no matter what your code does.
Examples:
NOT NULL — this column must always have a valueUNIQUE — no two rows can have the same value in this columnCHECK — value must match a condition (status IN ('pending', 'approved'))FOREIGN KEY — this value must exist in another tableWhy constraints beat app-level : Your code can have bugs. A DBA can run SQL directly. A script can bypass your API. The database cannot be bypassed. Constraints are your last line of defense.
Plain English: A group of database operations that either ALL succeed or ALL fail together.
Placing a food order with payment:
orders tableorder_items tableIf step 3 fails after steps 1 and 2 succeed, the restaurant has an unpaid order. A transaction ensures all three succeed or all three roll back.
await sequelize.transaction(async (tx) => {
const order = await Order.create({ userId, restaurantId, status: 'pending' }, { transaction: tx });
await OrderItem
Forgetting to pass { transaction: tx } to one of the calls inside a transaction block. That call runs outside the transaction and does NOT roll back.
Plain English: A pre-made set of database connections that requests share. Opening a database connection is slow (TCP handshake, authentication). The pool keeps 8–20 connections open and ready.
If your pool_max × pods exceeds Postgres's max_connections, new connections will be refused. This is a common outage cause on launch day.
Plain English: Doing the same thing twice has the same result as doing it once.
GET /orders/123 — call it 100 times, you always get the same order. Idempotent.
POST /orders — call it twice, you get two orders. NOT .
In payments, this matters hugely. If a network error causes the QuickBite app to retry a "charge customer" request, you don’t want to charge twice. The solution: Idempotency-Key headers — the server stores the result the first time and returns it on retry without executing again.
| Verb | Meaning | Idempotent? |
|---|---|---|
| GET | Read data, no side effects | Yes |
| POST | Create something new | No |
| PUT | Replace entire resource | Yes |
| PATCH | Update part of resource | Yes (usually) |
| DELETE | Remove resource | Yes |
Never use GET for operations with side effects. GET /orders/delete?id=123 is wrong — search engine crawlers can delete your data.
| Code | Meaning | When to use |
|---|---|---|
| 200 | OK | Successful GET, PATCH, DELETE |
| 201 | Created | Successful POST |
| 400 | Bad Request | Malformed request, missing required field |
| 401 | Unauthorized | No token, or token invalid |
| 403 | Forbidden | Token valid, but not allowed to do this |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Duplicate, or illegal transition |
| 422 | Unprocessable | Business rule fails (e.g. insufficient balance) |
| 500 | Internal Server Error | Your bug — unhandled exception |
The key distinction: 4xx = client's fault. 5xx = your fault. Never return 500 because the client sent a bad value.
Plain English: A unique ID attached to every request that follows it through every log line, across every service.
Without it: "find all log entries at 10:23:01.452 and hope nothing else happened that millisecond."
With it: grep cid=7f3a-abc... shows you every log line for exactly that one request.
Two endpoints every backend must have:
GET /healthz — "Am I alive?" Returns 200 if the process is running.GET /readyz — "Am I ready to serve traffic?" Returns 200 only if the DB is reachable.Don't make /healthz fail when the DB is down. If the DB is down, that's not YOUR process's fault — would restart you over and over. /healthz should only fail if your process itself is broken.
Plain English: A browser security feature that prevents websites you didn't authorize from calling your API.
If evil.com tries to call api.quickbite.app, the browser checks if api.quickbite.app has allowed evil.com. If not, the browser blocks the request.
Setting origin: true, credentials: true defeats entirely. Explicitly list allowed origins.
// ❌ Vulnerable: user input goes directly into SQL
const result = await sequelize.query(
`SELECT * FROM users WHERE email = '${email}'`
);
// If email = "x' OR '1'='1", this returns EVERY user
// ✅ Safe: parameterized query
const result = await sequelize.query(
`SELECT * FROM users WHERE email = $1`
Sequelize's model methods (.findAll, .findOne) are parameterized by default. The danger zone is raw SQL via sequelize.query().
The three-layer rule keeps everything testable: controller knows HTTP, service knows business rules, repository knows SQL. Cross that line once and the whole thing becomes a tangle.