Warming up the neural circuits...
By the end of this chapter you will:
This chapter is 100% hands-on. Every section adds one layer to the same endpoint. Don't skip ahead — each step builds on the last. Open your terminal and type along.
By the end of this chapter, you will have a working endpoint:
POST /api/v1/orders
Authorization: Bearer <jwt-token>
Body: { "restaurantId": "...", "items": [...] }
→ 201 { "id": "ord-abc", "status": "pending", "totalAmount": "24.99" }We'll build it in 7 layers, testing in Postman after each one.
Make sure you have these installed:
node --version # v20 or above
npm --version # v9 or above
psql --version # PostgreSQL 14+And create a database for this project:
psql -U postgres -c "CREATE DATABASE quickbite_dev;"npm install -g @nestjs/clinest new quickbite-apiNestJS asks you to pick a package manager. Choose npm.
? Which package manager would you ❤️ to use? npmThis creates a folder called quickbite-api. Open it:
cd quickbite-api
code .quickbite-api/
├── src/
│ ├── app.controller.ts ← default "hello world" controller (delete later)
│ ├── app.controller.spec.ts ← test for it
│ ├── app.module.ts ← root module — everything plugs in here
│ ├── app.service.ts ← default service
│ └── main.ts ← entry point — starts the HTTP server
├── test/
│ └── app.e2e-spec.ts ← E2E test skeleton
├── nest-cli.json ← CLI config
├── tsconfig.json ← TypeScript config
└── package.json// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory
npm run start:devYou should see:
[Nest] LOG [NestApplication] Nest application successfully started +XmsOpen Postman and send:
GET http://localhost:3000You should get back Hello World! — that's the default route from AppController.
Now you know the scaffold works. Let's build the real thing.
npm install @nestjs/sequelize sequelize sequelize-typescript pg pg-hstore
npm install --save-dev @types/sequelizenpm install @nestjs/configCreate a .env file in the root of the project:
# .env
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
DATABASE_NAME=quickbite_devAdd .env to your .gitignore immediately. Never commit credentials to git.
Open src/app.module.ts and replace its contents:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { SequelizeModule } from '@nestjs/sequelize';
@Module({
imports:
npm run start:devIf PostgreSQL is running and the credentials are correct, the app starts without errors. If you see a connection error, double-check your .env values and that PostgreSQL is running.
Run psql -U postgres -d quickbite_dev to verify you can connect manually with the same credentials.
We're going to create two tables: restaurants and orders.
npm install --save-dev sequelize-cliCreate a .sequelizerc file in the project root:
// .sequelizerc
const path = require('path');
module.exports = {
config: path.resolve('src/database', 'config.js'),
'models-path': path.
Create src/database/config.js:
// src/database/config.js
require('dotenv').config();
module.exports = {
development: {
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD
npx sequelize-cli migration:generate --name create-restaurants-and-ordersThis creates a file like src/database/migrations/20260502-create-restaurants-and-orders.js. Open it and replace the contents:
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.sequelize.transaction(async (t) => {
// ── restaurants ──────────────────────────────────
await queryInterface
npx sequelize-cli db:migrateYou should see:
== 20260502-create-restaurants-and-orders: migrating =======
== 20260502-create-restaurants-and-orders: migrated (0.845s)Verify the tables were created:
psql -U postgres -d quickbite_dev -c "\dt"Output:
List of relations
Schema │ Name │ Type │ Owner
────────┼─────────────────────┼───────┼──────────
public │ orders │ table │ postgres
public │ restaurants │ table │ postgres
public │ SequelizeMeta │ table │ postgres🎉 Tables exist. Now let's write the Sequelize models that map to them.
Create src/models/restaurant.model.ts:
// src/models/restaurant.model.ts
import {
Table, Column, Model, PrimaryKey, Default,
AllowNull, DataType, HasMany, CreatedAt, UpdatedAt,
} from 'sequelize-typescript';
import {
Create src/models/order.model.ts:
// src/models/order.model.ts
import {
Table, Column, Model, PrimaryKey, Default, AllowNull,
DataType, ForeignKey, BelongsTo, CreatedAt, UpdatedAt,
} from 'sequelize-typescript';
Register the models in AppModule by adding them to the SequelizeModule.forRootAsync config:
// In the useFactory return object, add:
models: [Restaurant, Order],And add the import at the top:
import { Restaurant } from './models/restaurant.model';
import { Order } from './models/order.model';NestJS CLI generates everything for you. Run:
nest generate module modules/orders
nest generate controller modules/orders
nest generate service modules/ordersThis creates:
src/modules/orders/
├── orders.module.ts
├── orders.controller.ts
├── orders.controller.spec.ts
└── orders.service.tsOpen src/modules/orders/orders.module.ts and register the Order model:
// src/modules/orders/orders.module.ts
import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { Order } from '../../models/order.model';
import { Restaurant } from '../../models/restaurant.model';
Add OrdersModule to AppModule:
// In AppModule's imports array:
import { OrdersModule } from './modules/orders/orders.module';
// Add to imports:
OrdersModule,Open src/modules/orders/orders.controller.ts:
// src/modules/orders/orders.controller.ts
import { Controller, Get } from '@nestjs/common';
@Controller('api/v1/orders')
export class OrdersController {
@Get()
findAll() {
return { message: 'Orders module is working! 🎉' };
GET http://localhost:3000/api/v1/ordersExpected response:
{ "message": "Orders module is working! 🎉" }If you get this, your module is wired up correctly. Move on.
A DTO (Data Transfer Object) defines the shape of the request body and validates it before your code even sees it.
npm install class-validator class-transformerOpen src/main.ts and update it:
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await
Create src/modules/orders/dto/create-order.dto.ts:
// src/modules/orders/dto/create-order.dto.ts
import {
IsUUID, IsArray, ArrayMinSize, ValidateNested,
IsInt, Min, IsString, IsOptional, MaxLength,
} from 'class-validator';
import { Type }
Update orders.controller.ts:
// src/modules/orders/orders.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { OrdersService } from './orders.service';
import { CreateOrderDto } from './dto/create-order.dto';
@
Test 1: Missing required field
POST http://localhost:3000/api/v1/orders
Content-Type: application/json
{}Expected: 400 Bad Request
{
"statusCode": 400,
"message": [
"restaurantId must be a valid UUID",
"items must be an array",
"You must order at least one item"
],
"error": "Bad Request"
}The DTO rejected the empty body before your code ran.
Test 2: Invalid UUID
{
"restaurantId": "not-a-uuid",
"items": [{ "menuItemId": "also-not-a-uuid", "quantity": 2 }]
}Expected: 400 with UUID errors.
Test 3: quantity is 0
{
"restaurantId": "550e8400-e29b-41d4-a716-446655440000",
"items": [{ "menuItemId": "550e8400-e29b-41d4-a716-446655440001", "quantity": 0 }]
}Expected: 400 — quantity must be at least 1.
Test 4: Valid body
{
"restaurantId": "550e8400-e29b-41d4-a716-446655440000",
"items": [
{ "menuItemId": "550e8400-e29b-41d4-a716-446655440001", "quantity": 2 }
],
"deliveryNote": "Leave at door please"
}Expected: 200 echoing back the validated DTO. Extra fields you add (like "hack": true) are stripped by whitelist: true.
Now let's make the endpoint actually create an order in the database.
Before we can create an order, we need a restaurant to reference. Let's insert one directly into the DB:
psql -U postgres -d quickbite_dev -c "
INSERT INTO restaurants (id, name, address, is_open)
VALUES (
'550e8400-e29b-41d4-a716-446655440000',
'Mario''s Pizza',
'12 Main Street, Bangalore',
true
);"Copy that UUID — you'll use it in every Postman request.
We don't return raw Sequelize models — we control exactly what the client sees.
Create src/modules/orders/dto/order.response.ts:
// src/modules/orders/dto/order.response.ts
import { Order } from '../../../models/order.model';
export class OrderResponse {
id: string;
status: string;
totalAmount: string;
restaurantId: string;
Open src/modules/orders/orders.service.ts:
// src/modules/orders/orders.service.ts
import {
Injectable, NotFoundException, BadRequestException, Logger,
} from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Order } from '../../models/order.model'
// src/modules/orders/orders.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { OrdersService } from './orders.service';
import { CreateOrderDto } from './dto/create-order.dto
Test 1: Create an order (happy path)
POST http://localhost:3000/api/v1/orders
Content-Type: application/json
{
"restaurantId": "550e8400-e29b-41d4-a716-446655440000",
"items": [
{ "menuItemId": "550e8400-e29b-41d4-a716-446655440001", "quantity": 2 },
{ "menuItemId": "550e8400-e29b-41d4-a716-446655440002", "quantity": 1 }
],
"deliveryNote": "Ring the bell twice"
}Expected: 201 Created
{
"id": "a3f1c2d4-...actual-uuid...",
"status": "pending",
"totalAmount": "15.00",
"restaurantId": "550e8400-e29b-41d4-a716-446655440000",
"deliveryNote": "Ring the bell twice",
"createdAt
Copy the id from the response.
Test 2: Retrieve the order
GET http://localhost:3000/api/v1/orders/<id-from-above>Expected: 200 with the same order.
Test 3: Order from a non-existent restaurant
{
"restaurantId": "00000000-0000-0000-0000-000000000000",
"items": [{ "menuItemId": "550e8400-e29b-41d4-a716-446655440001", "quantity": 1 }]
}Expected: 404 Not Found
{
"statusCode": 404,
"message": "Restaurant 00000000-0000-0000-0000-000000000000 not found"
}Test 4: Closed restaurant
Update the restaurant to be closed:
psql -U postgres -d quickbite_dev -c "
UPDATE restaurants SET is_open = false WHERE id = '550e8400-e29b-41d4-a716-446655440000';"Then try placing an order — you should get 400 Bad Request: Mario's Pizza is currently closed.
Set it back:
psql -U postgres -d quickbite_dev -c "
UPDATE restaurants SET is_open = true WHERE id = '550e8400-e29b-41d4-a716-446655440000';"Right now, anyone can create an order — no login required. Let's fix that.
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
npm install --save-dev @types/passport-jwtJWT_SECRET=super-secret-dev-key-change-in-production
JWT_EXPIRES_IN=24hCreate src/auth/jwt.strategy.ts:
// src/auth/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService
Create src/auth/auth.module.ts:
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ConfigModule, ConfigService } from '
Create src/auth/auth.service.ts — a simple token issuer for testing:
// src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
Create src/auth/auth.controller.ts:
// src/auth/auth.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('api/v1/auth')
export class AuthController {
constructor(
Add AuthModule to AppModule imports.
// src/modules/orders/orders.controller.ts
import {
Controller, Get, Post, Body, Param,
UseGuards, Request,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import
Step 1: Get a token
POST http://localhost:3000/api/v1/auth/login
Content-Type: application/json
{ "email": "test@example.com" }Response:
{ "access_token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOi..." }Copy the access_token.
Step 2: Try the orders endpoint WITHOUT a token
POST http://localhost:3000/api/v1/orders
Content-Type: application/json
{ "restaurantId": "...", "items": [...] }Expected: 401 Unauthorized — the guard rejected you.
Step 3: Try WITH the token
In Postman, go to the Authorization tab of your request:
Bearer Token<paste access_token here>Send the same request again. Expected: 201 Created — you're in.
Step 4: Try with an expired/fake token
Set the Authorization header to: Bearer fake.token.here
Expected: 401 Unauthorized.
Now that the skeleton works, here are exercises to deepen your understanding:
Add a findAll method to the service that returns all orders for the logged-in user. You'll need to pass req.user.id from the controller to the service.
Add a cancel endpoint. The service should:
pending or confirmed (you can't cancel an order that's already being delivered)status to cancelledRight now, each item costs $5. Make it real:
menu_items table migration with name, price, restaurant_idMenuItem modelOrdersService.create, look up each menu item's price and calculate totalAmount correctlyInstall @nestjs/swagger and add @ApiProperty() decorators to your DTOs. When you visit http://localhost:3000/api/docs, you'll see an interactive documentation page where you can test the API without Postman.
Every layer you added has exactly one job: DTO validates, service decides, controller routes, guard protects. When something breaks, you always know which layer to look at. That clarity is the entire point of NestJS's structure.
This is where your app starts. NestFactory.create(AppModule) builds the entire dependency injection graph. app.listen(3000) starts the HTTP server.