When Node.js applications grow beyond simple scripts or small APIs, structure becomes just as important as functionality. This is where NestJS shines. Built on top of Express (or Fastify), NestJS brings strong architectural patterns, dependency injection, and TypeScript-first design to the Node ecosystem.
When paired with TypeORM, you get a powerful combination for building clean, maintainable, database-driven REST APIs—without reinventing the wheel.
In this post, we’ll walk through building a REST API using NestJS and TypeORM, covering project setup, entities, repositories, services, controllers, and best practices you’ll want to apply in real-world applications.
What We’re Building
We’ll build a simple REST API for managing users, with full CRUD functionality:
- Create a user
- Retrieve all users
- Retrieve a single user
- Update a user
- Delete a user
Our stack:
- NestJS
- TypeScript
- TypeORM
- PostgreSQL (can easily be swapped for MySQL or SQLite)
Why NestJS + TypeORM?
Before diving into code, let’s talk about why this combination works so well.
NestJS
NestJS provides:
- Opinionated architecture
- Dependency injection out of the box
- Decorator-based syntax
- Excellent testing support
- TypeScript-first development
It feels familiar if you’ve worked with Angular, Spring, or ASP.NET Core.
TypeORM
TypeORM gives you:
- Entity-based database modeling
- Repository pattern
- Migrations
- Relationships
- Strong TypeScript integration
Together, they allow you to build APIs that scale cleanly as your project grows.
Step 1: Create a New NestJS Project
First, install the NestJS CLI:
npm install -g @nestjs/cli
Create a new project:
nest new nestjs-typeorm-api
Choose:
- npm or yarn
- TypeScript
Navigate into the project:
cd nestjs-typeorm-api
Step 2: Install TypeORM and Database Dependencies
Install TypeORM and PostgreSQL support:
npm install @nestjs/typeorm typeorm pg
Step 3: Configure TypeORM
Open app.module.ts and configure the database connection.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'nestjs_api',
autoLoadEntities: true,
synchronize: true,
}),
],
})
export class AppModule {}
Important notes
autoLoadEntities: trueautomatically registers entitiessynchronize: trueis great for development but should be disabled in production- Use environment variables for credentials in real apps
Step 4: Generate a Resource
NestJS can scaffold most of what we need.
nest generate resource users
Choose:
- REST API
- Yes to CRUD endpoints
This creates:
- Controller
- Service
- Module
- DTOs
A huge productivity win.
Step 5: Create the User Entity
Inside users/entities/user.entity.ts:
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column({ default: true })
isActive: boolean;
}
Why entities matter
Entities:
- Define your database schema
- Act as domain models
- Are strongly typed
- Support relationships and validations
Step 6: Register the Entity with TypeORM
In users.module.ts:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
This allows the User repository to be injected into services.
Step 7: Inject the Repository into the Service
Update users.service.ts:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
create(data: Partial<User>) {
const user = this.userRepository.create(data);
return this.userRepository.save(user);
}
findAll() {
return this.userRepository.find();
}
findOne(id: number) {
return this.userRepository.findOneBy({ id });
}
update(id: number, data: Partial<User>) {
return this.userRepository.update(id, data);
}
remove(id: number) {
return this.userRepository.delete(id);
}
}
Why the repository pattern works
- Abstracts raw SQL
- Keeps business logic clean
- Makes testing easier
- Supports advanced querying when needed
Step 8: Define DTOs for Input Validation
DTOs (Data Transfer Objects) protect your API from bad input.
create-user.dto.ts
export class CreateUserDto {
name: string;
email: string;
}
update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}
Later, you can add validation decorators like @IsEmail() or @IsNotEmpty().
Step 9: Implement the Controller
Update users.controller.ts:
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly 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);
}
@Patch(':id')
update(
@Param('id') id: string,
@Body() updateUserDto: UpdateUserDto,
) {
return this.usersService.update(+id, updateUserDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.usersService.remove(+id);
}
}
This controller is thin, readable, and focused purely on HTTP concerns.
Step 10: Run and Test the API
Start the server:
npm run start:dev
Test endpoints:
Create a user
POST /users
{
"name": "James",
"email": "james@example.com"
}
Get all users
GET /users
Update a user
PATCH /users/1
{
"name": "Updated Name"
}
Delete a user
DELETE /users/1
Common Mistakes to Avoid
❌ Using synchronize: true in production
Always use migrations in production.
❌ Putting business logic in controllers
Controllers should delegate to services.
❌ Skipping DTOs
DTOs protect your API from accidental data leaks.
❌ Not handling missing records
Throw proper HTTP exceptions (NotFoundException).
Why This Architecture Scales
NestJS with TypeORM scales well because:
- Modules isolate functionality
- Dependency injection keeps code testable
- Entities model your domain clearly
- Repositories abstract persistence
- Controllers stay thin and readable
This architecture works for:
- Monoliths
- Microservices
- Internal APIs
- Public REST APIs
Next Steps
Once your REST API is working, you can extend it with:
- Authentication (JWT, OAuth)
- Role-based access control
- Pagination and filtering
- Database migrations
- Caching with Redis
- Switching from Express to Fastify
Final Thoughts
Building a REST API with NestJS and TypeORM gives you structure without sacrificing flexibility. You get the discipline of enterprise frameworks with the agility of Node.js — a combination that works extremely well for modern backend systems.
If you’re tired of unstructured Express apps or want a backend framework that grows with your team, NestJS with TypeORM is one of the best choices available today.
If you’d like follow-up posts, I can cover:
- TypeORM relationships
- Database migrations
- NestJS authentication
- Testing with Jest
- Production deployment strategies
Just tell me what you’d like next.