logo
How to Build Production-Grade Applications with NestJS

How to Build Production-Grade Applications with NestJS

Dec 23, 2025

Introduction: When Simple Express Scripts Aren't Enough

You start with a simple Node.js project. A few Express routes, some database calls, maybe authentication. The code works, and you ship it.

Then your project grows.

Six months later:

  • You have 50 route handlers scattered across 10 files

  • Business logic is mixed everywhere (controllers, middleware, utility functions)

  • Adding a new feature requires changes in 5 different places

  • Tests are fragile because everything depends on everything else

  • New developers take 2 weeks to understand the codebase

  • A simple change breaks something unexpected

This is where NestJS comes in.

NestJS is a progressive backend framework for Node.js that brings enterprise-grade architecture to JavaScript development. It's built on top of Express (or Fastify), but adds structure, patterns, and tools that make large applications manageable.

Think of it like the difference between:

  • Express: Flexible toolkit (you decide the architecture)

  • NestJS: Complete framework (proven architecture, best practices built-in)


The Evolution of Node.js Backend Development

Stage 1: Simple Scripts (Express)

// routes.js - Everything mixed together
app.get('/users/:id', (req, res) => {
    const userId = req.params.id;
    // Business logic mixed with HTTP handling
    const user = database.query(`SELECT * FROM users WHERE id = ${userId}`);
    res.json(user);
});

app.post('/users', (req, res) => {
    // Validation mixed in
    if (!req.body.email) {
        return res.status(400).json({ error: 'Email required' });
    }
    // More business logic
    const user = database.create(req.body);
    res.json(user);
});

Problems:

  • ❌ Business logic mixed with HTTP concerns

  • ❌ Hard to test (everything depends on Express/HTTP)

  • ❌ Duplicated validation and error handling

  • ❌ Unclear code structure for new developers

  • ❌ Difficult to reuse logic across endpoints

Stage 2: Better Organized Express

// userController.js
exports.getUser = (req, res) => {
    const user = userService.findById(req.params.id);
    res.json(user);
};

// userService.js
exports.findById = (id) => {
    return database.query(`SELECT * FROM users WHERE id = ${id}`);
};

// routes.js
app.get('/users/:id', userController.getUser);
app.post('/users', userController.createUser);

Better, but still problems:

  • ❌ No built-in way to manage dependencies

  • ❌ Manual error handling everywhere

  • ❌ Testing requires mocking HTTP layer

  • ❌ Inconsistent patterns across the team

  • ❌ No built-in validation or serialization

  • ❌ Hard to add cross-cutting concerns (logging, auth)

Stage 3: NestJS (Production-Ready)

// user.service.ts
@Injectable()
export class UserService {
    constructor(private db: Database) {}
    
    findById(id: string) {
        return this.db.user.findUnique({ where: { id } });
    }
}

// user.controller.ts
@Controller('users')
export class UserController {
    constructor(private userService: UserService) {}
    
    @Get(':id')
    getUser(@Param('id') id: string) {
        return this.userService.findById(id);
    }
}

// user.module.ts
@Module({
    controllers: [UserController],
    providers: [UserService],
})
export class UserModule {}

Benefits:

  • ✅ Clear separation of concerns

  • ✅ Dependency injection (services are injected automatically)

  • ✅ Easy to test (just pass mock services)

  • ✅ Consistent patterns across the team

  • ✅ Built-in validation, error handling, logging

  • ✅ Scalable to thousands of lines of code


Why NestJS? The Problems It Solves

Problem 1: Unstructured Code

As Express apps grow, there's no standard structure. Different developers organize code differently, making onboarding and maintenance painful.

NestJS Solution: Enforces a proven structure with modules, controllers, and services.

my-app/
├── src/
│   ├── users/
│   │   ├── user.controller.ts
│   │   ├── user.service.ts
│   │   ├── user.entity.ts
│   │   ├── dto/
│   │   │   ├── create-user.dto.ts
│   │   │   └── update-user.dto.ts
│   │   └── user.module.ts
│   ├── posts/
│   │   ├── post.controller.ts
│   │   ├── post.service.ts
│   │   ├── post.entity.ts
│   │   └── post.module.ts
│   └── app.module.ts

Every developer knows where to look. New features follow the same pattern.

Problem 2: Hard-to-Scale Architecture

In a poorly structured Express app, adding features becomes harder as the codebase grows. There's no clear way to organize different parts of the application.

NestJS Solution: Modular architecture where features are isolated in modules that can be developed, tested, and deployed independently.

Instead of:
app.js (500 lines of everything)

You get:
auth.module.ts (Auth feature)
users.module.ts (User feature)
posts.module.ts (Post feature)
analytics.module.ts (Analytics feature)

Each module is self-contained and can be maintained independently.

Problem 3: Dependency Management

Passing dependencies around (services, database connections, etc.) becomes messy in Express.

// ❌ Manual dependency passing
const db = require('./database');
const cache = require('./cache');
const logger = require('./logger');

const userService = new UserService(db, cache, logger);
const userController = new UserController(userService);
const postService = new PostService(db, cache, logger);
const postController = new PostController(postService);

// This gets out of hand quickly!

NestJS Solution: Automatic dependency injection. You declare what you need, and NestJS provides it.

// ✅ Dependencies are injected automatically
@Injectable()
export class UserService {
    constructor(
        private db: Database,
        private cache: CacheService,
        private logger: LoggerService
    ) {}
}

// NestJS handles instantiation and injection

Problem 4: Inconsistent Patterns

Different developers handle authentication, error handling, validation, and logging differently, leading to bugs and confusion.

NestJS Solution: Enforces consistent patterns through Guards, Pipes, and Interceptors.

// ✅ Consistent authentication
@Controller('users')
@UseGuards(JwtAuthGuard)
export class UserController {
    @Get()
    getUsers() { ... }
}

// ✅ Consistent validation
@Post()
createUser(@Body(new ValidationPipe()) createUserDto: CreateUserDto) { ... }

// ✅ Consistent error handling
// Done via exception filters that all errors pass through

What NestJS Gives You

1. Opinionated Structure

NestJS has a well-defined way to organize code. Everyone on the team follows the same patterns, making code reviews, onboarding, and maintenance easier.

Structure:

  • Controllers: Handle HTTP requests/responses

  • Services: Contain business logic

  • Modules: Group related features

  • Entities: Database models

  • DTOs: Data validation and transformation

  • Guards: Authentication and authorization

  • Interceptors: Cross-cutting concerns (logging, error handling)

  • Pipes: Data validation and transformation

2. Built-in Dependency Injection

NestJS automatically manages object creation and dependency injection. You declare what you need, and it's provided automatically.

Benefits:

  • Cleaner code (no manual instantiation)

  • Easier testing (swap real services with mocks)

  • Better loose coupling between components

  • Easier to add new dependencies

3. TypeScript First

NestJS is built for TypeScript (not just JavaScript). This means:

FeatureBenefit
Type safetyCatch errors at compile time, not runtime
IDE supportAuto-complete, refactoring, finding usages
Self-documenting codeTypes serve as inline documentation
Better testingMocking is type-safe

4. Modular Architecture

Applications are divided into modules, each responsible for a specific feature.

@Module({
    controllers: [UserController],
    providers: [UserService],
    exports: [UserService], // Other modules can use this
})
export class UserModule {}

@Module({
    imports: [UserModule],  // Import and use UserService
    controllers: [PostController],
    providers: [PostService],
})
export class PostModule {}

Benefits:

  • Easy to understand (each module is a feature)

  • Easy to test (modules can be tested independently)

  • Easy to scale (add modules without affecting others)

  • Easy to maintain (changes in one module don't break others)

5. First-Class Support for Common Patterns

NestJS provides built-in solutions for things that are painful in Express:

FeatureWhat It Is
AuthenticationGuards and strategies for JWT, OAuth, etc.
ValidationPipes that validate DTOs automatically
Error HandlingException filters that handle all errors consistently
LoggingBuilt-in logger and integration with popular log services
Database IntegrationSeamless integration with TypeORM, Prisma, etc.
API DocumentationAutomatic Swagger/OpenAPI generation
CachingBuilt-in caching decorators
WebSocketsFirst-class support for real-time features
MicroservicesPattern for building distributed systems
TestingTest utilities and patterns built-in

6. Built for Teams

The structure and patterns make onboarding new developers much easier.

Developer onboarding timeline:

  • Express: 2-3 weeks to understand the codebase architecture

  • NestJS: 2-3 days (everyone follows the same patterns)


Core Architecture Concepts

Before we build something, let's understand how NestJS applications work.

Modules: Organizing Features

Think of modules as containers for related features. Each module is self-contained.

// user.module.ts
@Module({
    controllers: [UserController],      // HTTP routes
    providers: [UserService],            // Business logic
    exports: [UserService],              // Make available to other modules
})
export class UserModule {}

// app.module.ts (Root module)
@Module({
    imports: [UserModule, PostModule],   // Include other modules
})
export class AppModule {}

Example structure:

users/ (module)
  ├── user.controller.ts
  ├── user.service.ts
  ├── user.entity.ts
  ├── dto/
  └── user.module.ts

posts/ (module)
  ├── post.controller.ts
  ├── post.service.ts
  ├── post.entity.ts
  └── post.module.ts

comments/ (module)
  ├── comment.controller.ts
  ├── comment.service.ts
  └── comment.module.ts

Each module can be worked on independently.

Controllers: Handling HTTP Requests

Controllers receive HTTP requests and send responses. They shouldn't contain business logic.

@Controller('users')  // Routes: /users
export class UserController {
    constructor(private userService: UserService) {}
    
    @Get()                           // GET /users
    getAllUsers() {
        return this.userService.findAll();
    }
    
    @Get(':id')                      // GET /users/:id
    getUserById(@Param('id') id: string) {
        return this.userService.findById(id);
    }
    
    @Post()                          // POST /users
    createUser(@Body() createUserDto: CreateUserDto) {
        return this.userService.create(createUserDto);
    }
    
    @Put(':id')                      // PUT /users/:id
    updateUser(
        @Param('id') id: string,
        @Body() updateUserDto: UpdateUserDto
    ) {
        return this.userService.update(id, updateUserDto);
    }
    
    @Delete(':id')                   // DELETE /users/:id
    deleteUser(@Param('id') id: string) {
        return this.userService.delete(id);
    }
}

Key points:

  • Controllers are thin (just HTTP handling)

  • Business logic is in services

  • Parameters are extracted declaratively (@Param, @Body, @Query)

  • Return values are automatically serialized to JSON

Services: Business Logic

Services contain the actual business logic. They're reusable across different controllers.

@Injectable()
export class UserService {
    constructor(private prisma: PrismaService) {}
    
    findAll() {
        return this.prisma.user.findMany();
    }
    
    findById(id: string) {
        return this.prisma.user.findUnique({
            where: { id }
        });
    }
    
    create(data: CreateUserDto) {
        return this.prisma.user.create({ data });
    }
    
    update(id: string, data: UpdateUserDto) {
        return this.prisma.user.update({
            where: { id },
            data
        });
    }
    
    delete(id: string) {
        return this.prisma.user.delete({
            where: { id }
        });
    }
}

Key points:

  • Services are marked with @Injectable() (they can be injected)

  • Services are reusable (multiple controllers can use the same service)

  • Services contain no HTTP concerns

  • Services are easy to test (just pass a mock database)

Dependency Injection: Automatic Dependency Management

Instead of manually creating and passing objects, NestJS handles it for you.

Without Dependency Injection (manual):

// ❌ Manual - hard to manage and test
const database = new Database();
const cache = new Cache();
const logger = new Logger();
const userService = new UserService(database, cache, logger);
const userController = new UserController(userService);

With Dependency Injection (NestJS):

// ✅ Automatic - NestJS handles it
@Injectable()
export class UserService {
    constructor(
        private db: Database,
        private cache: Cache,
        private logger: Logger
    ) {}
}

@Controller('users')
export class UserController {
    constructor(private userService: UserService) {}
}

// NestJS creates instances in the right order and injects them

Benefits:

  • Less boilerplate code

  • Easy to test (inject mock services)

  • Easy to change implementations (just provide a different service)

  • Automatic handling of circular dependencies


Real-World Production Features

A real-world backend needs more than just routes. NestJS is designed to handle these concerns cleanly.

Authentication and Authorization

Authentication: Who is the user? Authorization: What can the user do?

NestJS Approach:

// jwt.strategy.ts - Define how to validate JWT tokens
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
    constructor(private configService: ConfigService) {
        super({
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            ignoreExpiration: false,
            secretOrKey: configService.get('JWT_SECRET'),
        });
    }

    validate(payload: any) {
        return { userId: payload.sub, username: payload.username };
    }
}

// jwt-auth.guard.ts - Protect routes with JWT
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

// user.controller.ts - Use the guard
@Controller('users')
export class UserController {
    @Get('profile')
    @UseGuards(JwtAuthGuard)  // Require JWT token
    getProfile(@Request() req) {
        return req.user;
    }
}

Benefits:

  • Authentication is centralized

  • Guards are reusable across controllers

  • Easy to test (mock the guard)

  • Consistent across the application

Data Validation

NestJS automatically validates request data using DTOs and Pipes.

// create-user.dto.ts - Define expected request structure
export class CreateUserDto {
    @IsEmail()
    email: string;

    @MinLength(6)
    password: string;

    @IsOptional()
    @IsPhoneNumber()
    phone: string;
}

// user.controller.ts - Validate automatically
@Controller('users')
export class UserController {
    @Post()
    createUser(@Body() createUserDto: CreateUserDto) {
        // ✅ createUserDto is validated and typed
        // Invalid requests are rejected automatically
        return this.userService.create(createUserDto);
    }
}

What NestJS does:

  1. Receives request

  2. Validates against CreateUserDto schema

  3. Transforms to typed object

  4. Rejects if invalid

  5. Passes to controller only if valid

Benefits:

  • Validation is declarative

  • Errors are consistent

  • No manual validation code in controllers

  • Type-safe throughout

Database Integration

NestJS integrates seamlessly with popular ORMs like TypeORM and Prisma.

Example with TypeORM:

// user.entity.ts - Database model
@Entity()
export class User {
    @PrimaryGeneratedColumn('uuid')
    id: string;

    @Column({ unique: true })
    email: string;

    @Column()
    password: string;

    @Column({ default: new Date() })
    createdAt: Date;
}

// user.module.ts - Register with database
@Module({
    imports: [TypeOrmModule.forFeature([User])],
    controllers: [UserController],
    providers: [UserService],
})
export class UserModule {}

// user.service.ts - Use automatically
@Injectable()
export class UserService {
    constructor(
        @InjectRepository(User)
        private userRepository: Repository<User>,
    ) {}

    findAll() {
        return this.userRepository.find();
    }

    findById(id: string) {
        return this.userRepository.findOne({ where: { id } });
    }

    create(data: CreateUserDto) {
        const user = this.userRepository.create(data);
        return this.userRepository.save(user);
    }
}

Benefits:

  • Type-safe database queries

  • Schema is defined in code

  • Migrations are managed

  • Relationship management is automatic

Error Handling

Errors are handled consistently through Exception Filters.

// http-exception.filter.ts - Global error handler
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        const status = exception.getStatus();
        const exceptionResponse = exception.getResponse();

        response
            .status(status)
            .json({
                statusCode: status,
                timestamp: new Date().toISOString(),
                path: ctx.getRequest().url,
                message: exceptionResponse,
            });
    }
}

// main.ts - Register globally
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);

// user.controller.ts - Throw errors, they're handled automatically
@Controller('users')
export class UserController {
    @Get(':id')
    async getUser(@Param('id') id: string) {
        const user = await this.userService.findById(id);
        if (!user) {
            throw new NotFoundException('User not found');
        }
        return user;
    }
}

Benefits:

  • Errors are handled consistently

  • No try-catch blocks everywhere

  • Error responses are formatted uniformly

  • Logging is centralized

Logging

NestJS provides a built-in logger and integrates with popular logging services.

@Injectable()
export class UserService {
    private logger = new Logger(UserService.name);

    findAll() {
        this.logger.log('Fetching all users');
        const users = this.userRepository.find();
        this.logger.debug(`Found ${users.length} users`);
        return users;
    }

    async createUser(createUserDto: CreateUserDto) {
        this.logger.log(`Creating user with email: ${createUserDto.email}`);
        try {
            const user = await this.userRepository.create(createUserDto);
            this.logger.log(`User created with ID: ${user.id}`);
            return user;
        } catch (error) {
            this.logger.error(`Failed to create user: ${error.message}`);
            throw error;
        }
    }
}

Benefits:

  • Structured logging

  • Different log levels (log, debug, warn, error)

  • Integrated across the application

  • Easy to export to logging services (Winston, Bunyan, etc.)

Real-Time Communication (WebSockets)

For features like notifications and live updates, NestJS supports WebSockets as a first-class feature.

@WebSocketGateway()
export class NotificationGateway implements OnGatewayConnection {
    @WebSocketServer()
    server: Server;

    handleConnection(client: Socket) {
        console.log(`Client connected: ${client.id}`);
    }

    @SubscribeMessage('message')
    handleMessage(client: Socket, data: string) {
        this.server.emit('message', data);
    }

    sendNotification(userId: string, message: string) {
        this.server.to(userId).emit('notification', message);
    }
}

// user.service.ts
@Injectable()
export class UserService {
    constructor(private notificationGateway: NotificationGateway) {}

    async updateUser(id: string, data: UpdateUserDto) {
        const user = await this.userRepository.update(id, data);
        this.notificationGateway.sendNotification(id, 'Profile updated');
        return user;
    }
}

Benefits:

  • Real-time features are first-class

  • Clean separation from HTTP logic

  • Easy to test

  • Scalable with message queues

Rate Limiting

Protect APIs from abuse with rate limiting.

// throttler.guard.ts
@Injectable()
export class ThrottlerGuard extends AbstractHttpGuard {
    async canActivate(context: ExecutionContext): Promise<boolean> {
        // Limit to 10 requests per minute
        return super.canActivate(context);
    }
}

// user.controller.ts
@Controller('users')
export class UserController {
    @Get()
    @UseGuards(ThrottlerGuard)
    getUsers() {
        return this.userService.findAll();
    }

    @Post()
    @UseGuards(ThrottlerGuard)
    createUser(@Body() createUserDto: CreateUserDto) {
        return this.userService.create(createUserDto);
    }
}

Getting Started with NestJS

Prerequisites

You need:

  • Node.js 18+ (check with node -v)

  • npm or yarn (comes with Node.js)

  • Code editor (VS Code recommended)

  • Basic TypeScript knowledge (NestJS teaches you as you go)

Step 1: Install NestJS CLI

npm install -g @nestjs/cli

Step 2: Create a New Project

nest new my-backend
cd my-backend

Choose during setup:

  • Package manager: npm, yarn, or pnpm

  • Default options are fine for learning

Step 3: Understand the Project Structure

src/
├── main.ts                 # Application entry point
├── app.module.ts          # Root module
├── app.controller.ts      # Example controller
├── app.service.ts         # Example service
test/                       # Test files

Step 4: Run the Application

npm run start:dev

Visit http://localhost:3000 in your browser.

You should see: Hello World!

Step 5: Generate Your First Feature

# Generate a complete user module (controller + service + module)
nest generate module users
nest generate controller users
nest generate service users

This creates:

src/users/
├── users.module.ts
├── users.controller.ts
├── users.service.ts

Step 6: Create a Database Entity

If using TypeORM:

// users/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @Column({ unique: true })
    email: string;

    @Column()
    password: string;
}

Step 7: Create a DTO (Data Transfer Object)

// users/dto/create-user.dto.ts
import { IsEmail, MinLength } from 'class-validator';

export class CreateUserDto {
    name: string;

    @IsEmail()
    email: string;

    @MinLength(6)
    password: string;
}

Step 8: Implement the Service

// users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UsersService {
    constructor(
        @InjectRepository(User)
        private usersRepository: Repository<User>,
    ) {}

    create(createUserDto: CreateUserDto) {
        const user = this.usersRepository.create(createUserDto);
        return this.usersRepository.save(user);
    }

    findAll() {
        return this.usersRepository.find();
    }

    findOne(id: number) {
        return this.usersRepository.findOne({ where: { id } });
    }

    update(id: number, updateUserDto: CreateUserDto) {
        return this.usersRepository.update(id, updateUserDto);
    }

    remove(id: number) {
        return this.usersRepository.delete(id);
    }
}

Step 9: Implement the Controller

// users/users.controller.ts
import { Controller, Get, Post, Body, Param, Delete } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UsersController {
    constructor(private usersService: UsersService) {}

    @Post()
    create(@Body() createUserDto: CreateUserDto) {
        return this.usersService.create(createUserDto);
    }

    @Get()
    findAll() {
        return this.usersService.findAll();
    }

    @Get(':id')
    findOne(@Param('id') id: string) {
        return this.usersService.findOne(+id);
    }

    @Delete(':id')
    remove(@Param('id') id: string) {
        return this.usersService.remove(+id);
    }
}

NestJS vs Express: A Comparison

FeatureExpressNestJS
Learning CurveShallow (easy to start)Moderate (more concepts)
StructureYou decideOpinionated, enforced
ScalabilityDepends on team disciplineBuilt-in scalability patterns
TypeScriptOptional, needs setupFirst-class, built-in
Dependency InjectionNo (manual)Yes (automatic)
ValidationManual or via middlewareBuilt-in with Pipes
Error HandlingManual everywhereCentralized Exception Filters
TestingHarder (HTTP layer tangled)Easier (clean separation)
AuthenticationManual or via passportGuards + Strategies
DatabaseAny ORM, you manageIntegrated TypeORM/Prisma
Suitable ForSmall projects, APIsProduction systems, teams

When to use Express:

  • Small projects

  • Prototypes

  • Learning Node.js basics

  • When you want complete flexibility

When to use NestJS:

  • Production applications

  • Large teams

  • Long-term maintenance

  • Microservices

  • Enterprise requirements


Key Takeaways

  1. NestJS brings structure to Node.js development - No more figuring out how to organize your code. Follow the pattern.

  2. Dependency Injection simplifies everything - Easier to test, easier to refactor, less boilerplate.

  3. TypeScript first means fewer bugs - Catch errors at compile time, not in production.

  4. Modules make applications scalable - Each feature is isolated and independently testable.

  5. Built-in solutions for common problems - Authentication, validation, error handling, logging, all built-in.

  6. Great for teams - New developers understand the code structure immediately.

  7. Enterprise-grade but developer-friendly - The power of Spring Boot or ASP.NET Core, but with Node.js and JavaScript.


Next Steps

Once you understand NestJS basics:

  1. Build a complete application - Users, posts, comments, with authentication

  2. Add a database - PostgreSQL with TypeORM or Prisma

  3. Implement authentication - JWT tokens, refresh tokens

  4. Add validation - Request validation with class-validator

  5. Write tests - Unit tests for services, integration tests for controllers

  6. Deploy to production - Using Caddy and PM2 (from the earlier guide)

  7. Add advanced features - WebSockets, caching, background jobs


Further Reading


Common Misconceptions About NestJS

"NestJS is heavyweight"

False. NestJS itself is lightweight. You only pay for what you use. Add features as needed.

"NestJS is slow"

False. NestJS is built on Express/Fastify, which are very fast. The framework adds minimal overhead.

"NestJS is only for large projects"

False. While NestJS shines in large projects, it's great for medium and small projects too. The structure helps even with small codebases.

"I have to use TypeScript with NestJS"

False. You can use plain JavaScript, but TypeScript is strongly recommended for full benefits.

"NestJS ties you to one framework"

False. NestJS works with Express OR Fastify. You can switch with one line of code.


Real-World Example: Building a Blog API

To see everything together, here's a minimal blog API:

Structure:

src/
├── posts/
│   ├── posts.controller.ts
│   ├── posts.service.ts
│   ├── post.entity.ts
│   ├── dto/create-post.dto.ts
│   └── posts.module.ts
├── comments/
│   ├── comments.controller.ts
│   ├── comments.service.ts
│   ├── comment.entity.ts
│   └── comments.module.ts
├── app.module.ts
└── main.ts

post.entity.ts:

@Entity()
export class Post {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    title: string;

    @Column('text')
    content: string;

    @Column({ default: new Date() })
    createdAt: Date;
}

posts.controller.ts:

@Controller('posts')
export class PostsController {
    constructor(private postsService: PostsService) {}

    @Get()
    findAll() {
        return this.postsService.findAll();
    }

    @Post()
    create(@Body() createPostDto: CreatePostDto) {
        return this.postsService.create(createPostDto);
    }
}

posts.service.ts:

@Injectable()
export class PostsService {
    constructor(
        @InjectRepository(Post)
        private postsRepository: Repository<Post>,
    ) {}

    create(createPostDto: CreatePostDto) {
        const post = this.postsRepository.create(createPostDto);
        return this.postsRepository.save(post);
    }

    findAll() {
        return this.postsRepository.find({
            relations: ['comments'],
            order: { createdAt: 'DESC' },
        });
    }
}

That's it! You have a fully functional, type-safe, testable API with minimal code. Add database, authentication, validation, and you have a production-ready blog API.