Backend Development
Notes from the field

Building Your First REST API with NestJS: A Step-by-Step Tutorial

Putra Prassiesa Abimanyu
Thu Oct 23 2025
6 min read
NestJSTypeScriptREST APINode.jsBackendTutorialGetting Started
Share:
Building Your First REST API with NestJS: A Step-by-Step Tutorial

A beginner-friendly guide to creating your first REST API using NestJS and TypeScript. Learn how to set up a NestJS project from scratch, understand modules and controllers, create CRUD endpoints, validate data with DTOs, connect to a database using TypeORM, and implement basic authentication. This practical tutorial walks you through building a simple blog API while explaining NestJS core concepts in an approachable way, perfect for junior developers transitioning from Express.js or learning backend development.

Building Your First REST API with NestJS: A Step-by-Step Tutorial

When I first heard about NestJS, I was a bit intimidated. Coming from Express.js, all the decorators, modules, and TypeScript syntax looked complicated. But after building my first API, I realized NestJS actually makes things easier—it just has a learning curve at the start.

In this guide, I'll show you how to build a simple blog API with NestJS. We'll cover the basics without overwhelming you with advanced concepts. By the end, you'll have a working API and understand how NestJS organizes code.

What is NestJS and Why Use It?

NestJS is a framework built on top of Express.js (or Fastify) that adds structure to your Node.js applications. Think of it like this:

  • Express.js: You decide how to organize everything
  • NestJS: It gives you a proven structure to follow

The main benefits I've found:

  • TypeScript by default - Catches errors before you run the code
  • Built-in organization - No more wondering where to put files
  • Great for teams - Everyone follows the same patterns
  • Lots of features - Authentication, validation, database connections all included

If you're building something serious that will grow, NestJS is worth learning.

What You'll Need

Before starting, make sure you have:

  • Node.js installed (version 16 or higher)
  • Basic TypeScript knowledge (don't worry if you're still learning)
  • Familiarity with REST APIs
  • A code editor (VS Code works great)

That's it! Let's build something.

Setting Up Your First NestJS Project

NestJS has a CLI that makes setup super easy. Install it globally:

npm install -g @nestjs/cli

Now create a new project:

nest new blog-api

The CLI will ask which package manager you want. I use npm, but choose whatever you prefer. It'll create a project with this structure:

blog-api/
├── src/
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test/
├── package.json
└── tsconfig.json

Let's start the development server:

cd blog-api
npm run start:dev

Visit http://localhost:3000 and you should see "Hello World!". Your NestJS app is running!

Understanding the Basics

Before we build features, let's understand the key pieces:

Modules

Modules organize your code into logical groups. Every app has at least one module (AppModule).

Controllers

Controllers handle HTTP requests. They receive requests and return responses.

Services

Services contain your business logic. Controllers call services to do the actual work.

This separation keeps code organized. Controllers are thin (just handle HTTP stuff), services are fat (contain the logic).

Building a Posts Module

Let's build a simple blog posts API. We'll create endpoints to:

  • Get all posts
  • Get a single post
  • Create a post
  • Update a post
  • Delete a post

Generate a new module using the CLI:

nest generate module posts
nest generate controller posts
nest generate service posts

The CLI creates files and automatically imports them. Nice!

Creating Your First Endpoint

Open src/posts/posts.controller.ts and let's create a route:

import { Controller, Get } from '@nestjs/common';

@Controller('posts')
export class PostsController {
  @Get()
  findAll() {
    return { message: 'This will return all posts' };
  }
}

Visit http://localhost:3000/posts and you'll see the message. That's it! The @Get() decorator creates a GET endpoint.

Adding a Service

Controllers should be simple. Let's move the logic to a service. Open src/posts/posts.service.ts:

import { Injectable } from '@nestjs/common';

@Injectable()
export class PostsService {
  private posts = [
    { id: 1, title: 'First Post', content: 'Hello World!' },
    { id: 2, title: 'Second Post', content: 'Learning NestJS' },
  ];

  findAll() {
    return this.posts;
  }

  findOne(id: number) {
    return this.posts.find(post => post.id === id);
  }
}

Now update the controller to use the service:

import { Controller, Get, Param } from '@nestjs/common';
import { PostsService } from './posts.service';

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

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

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

Notice the constructor? That's dependency injection. NestJS automatically gives us an instance of PostsService. Cool, right?

Creating Posts

Let's add the ability to create posts. First, we need a DTO (Data Transfer Object) to define what data we expect.

Create src/posts/dto/create-post.dto.ts:

export class CreatePostDto {
  title: string;
  content: string;
}

Add validation by installing class-validator:

npm install class-validator class-transformer

Update the DTO:

import { IsNotEmpty, IsString } from 'class-validator';

export class CreatePostDto {
  @IsNotEmpty()
  @IsString()
  title: string;

  @IsNotEmpty()
  @IsString()
  content: string;
}

Enable validation globally in src/main.ts:

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  app.useGlobalPipes(new ValidationPipe());
  
  await app.listen(3000);
}
bootstrap();

Now add the create method to your service:

create(createPostDto: CreatePostDto) {
  const newPost = {
    id: this.posts.length + 1,
    ...createPostDto,
  };
  this.posts.push(newPost);
  return newPost;
}

And to your controller:

import { Controller, Get, Post, Body, Param } from '@nestjs/common';

@Controller('posts')
export class PostsController {
  // ... existing code ...

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

Test it with curl:

curl -X POST http://localhost:3000/posts \
  -H "Content-Type: application/json" \
  -d '{"title": "My Post", "content": "This is my post content"}'

If you send invalid data (like an empty title), you'll get a validation error. That's the validation working!

Adding Update and Delete

Let's complete our CRUD operations:

// In posts.service.ts
update(id: number, updatePostDto: CreatePostDto) {
  const postIndex = this.posts.findIndex(post => post.id === id);
  if (postIndex === -1) {
    return null;
  }
  this.posts[postIndex] = { ...this.posts[postIndex], ...updatePostDto };
  return this.posts[postIndex];
}

delete(id: number) {
  const postIndex = this.posts.findIndex(post => post.id === id);
  if (postIndex === -1) {
    return false;
  }
  this.posts.splice(postIndex, 1);
  return true;
}
// In posts.controller.ts
import { Controller, Get, Post, Body, Param, Put, Delete, NotFoundException } from '@nestjs/common';

@Controller('posts')
export class PostsController {
  // ... existing code ...

  @Put(':id')
  update(@Param('id') id: string, @Body() updatePostDto: CreatePostDto) {
    const post = this.postsService.update(+id, updatePostDto);
    if (!post) {
      throw new NotFoundException(`Post with ID ${id} not found`);
    }
    return post;
  }

  @Delete(':id')
  delete(@Param('id') id: string) {
    const deleted = this.postsService.delete(+id);
    if (!deleted) {
      throw new NotFoundException(`Post with ID ${id} not found`);
    }
    return { message: 'Post deleted successfully' };
  }
}

Now you have a complete CRUD API!

Adding a Database

Storing data in memory is fine for learning, but it disappears when you restart the server. Let's add a real database with TypeORM.

Install the packages:

npm install @nestjs/typeorm typeorm sqlite3

I'm using SQLite because it's simple—no database server to set up. For production, you'd use PostgreSQL or MySQL.

Create src/posts/entities/post.entity.ts:

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

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

  @Column()
  title: string;

  @Column()
  content: string;
}

Update src/app.module.ts:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PostsModule } from './posts/posts.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite',
      database: 'blog.db',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true, // Only for development!
    }),
    PostsModule,
  ],
})
export class AppModule {}

Update src/posts/posts.module.ts:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
import { Post } from './entities/post.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Post])],
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}

Finally, update the service to use the database:

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Post } from './entities/post.entity';
import { CreatePostDto } from './dto/create-post.dto';

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

  async findAll(): Promise<Post[]> {
    return await this.postsRepository.find();
  }

  async findOne(id: number): Promise<Post> {
    const post = await this.postsRepository.findOne({ where: { id } });
    if (!post) {
      throw new NotFoundException(`Post with ID ${id} not found`);
    }
    return post;
  }

  async create(createPostDto: CreatePostDto): Promise<Post> {
    const post = this.postsRepository.create(createPostDto);
    return await this.postsRepository.save(post);
  }

  async update(id: number, updatePostDto: CreatePostDto): Promise<Post> {
    await this.postsRepository.update(id, updatePostDto);
    return this.findOne(id);
  }

  async delete(id: number): Promise<void> {
    const result = await this.postsRepository.delete(id);
    if (result.affected === 0) {
      throw new NotFoundException(`Post with ID ${id} not found`);
    }
  }
}

Now your data persists! Restart the server and your posts will still be there.

What You've Learned

Congratulations! You just built a REST API with NestJS. Let's recap:

  • Project setup with the NestJS CLI
  • Modules organize your code
  • Controllers handle HTTP requests
  • Services contain business logic
  • DTOs define and validate data
  • TypeORM connects to databases
  • Decorators like @Get(), @Post(), etc.

Next Steps

You've got the basics down. Here's what to learn next:

  • Authentication - JWT tokens for securing endpoints
  • Environment variables - Using @nestjs/config
  • Relations - Connecting posts to users
  • Error handling - Custom exception filters
  • Testing - Writing unit and e2e tests

My Tips for Learning NestJS

1. Don't compare it to Express too much

NestJS is different. Embrace the structure instead of fighting it.

2. Use the CLI

The nest generate command saves so much time and follows best practices.

3. Read the docs

NestJS has excellent documentation at docs.nestjs.com. It's actually readable!

4. Start simple

Don't try to learn everything at once. Build basic APIs first, then add complexity.

5. Practice with real projects

Theory only goes so far. Build small projects to solidify your understanding.

Final Thoughts

NestJS felt weird at first, but now I appreciate the structure it provides. When I go back to Express projects, I actually miss having a clear place for everything.

The learning curve is real, but it's not as steep as it looks. Start with what we covered here, build a few simple APIs, and gradually explore more features.

Remember: every expert was once a beginner who didn't give up. You've got this!

Happy coding!

Building Your First REST API with NestJS: A Step-by-Step Tutorial