Wistant Logo
Command Palette

Search for a command to run...

0
Blog

NestJS for Beginners: Building Structured Backend Applications with Node.js and TypeScript

A beginner-friendly guide to transitioning from Express.js to NestJS. Understand Node.js fundamentals, why structured architectures matter, and how to build your first NestJS API.

NestJS for Beginners: Building Structured Backend Applications with Node.js and TypeScript

For developers transitioning from frontend development to the backend, or for JavaScript developers looking to build robust servers, the Node.js ecosystem offers unparalleled flexibility. However, that very flexibility can become a double-edged sword when building large-scale applications.

To understand why NestJS is so popular, we must first look at the foundations it is built upon: Node.js and the evolution of its web server frameworks.


The Node.js Foundation: Server-Side JavaScript

Before Node.js was released in 2009, JavaScript was confined to running inside web browsers. Node.js changed this by wrapping Google Chrome's V8 JavaScript engine in a runtime environment that runs directly on your operating system.

Node.js Engine Execution Environment Logo

Node.js introduced several core advantages:

  • Single Language Stack: Teams could write both frontend and backend code in JavaScript (and later, TypeScript).
  • Asynchronous, Non-blocking I/O: Instead of creating a new thread for every incoming request (which consumes significant RAM), Node.js uses a single-threaded event loop to delegate database and filesystem operations to the operating system, pulling the results back when ready.
  • The NPM Ecosystem: Developers gained access to the largest registry of open-source libraries in the world.

To write a web server in raw Node.js, you would write code like this:

const http = require("http")
 
const server = http.createServer((req, res) => {
  if (req.url === "/" && req.method === "GET") {
    res.writeHead(200, { "Content-Type": "text/plain" })
    res.end("Hello from raw Node.js")
  } else {
    res.writeHead(404)
    res.end("Not Found")
  }
})
 
server.listen(3000)

While this works, parsing URL query parameters, handling JSON request bodies, and organizing complex routing using raw Node.js code quickly becomes unmanageable.


The Express.js Era: Lightweight Routing and Middleware

To solve the complexity of raw Node.js HTTP servers, Express.js was released. It became the default web server library for the Node.js ecosystem.

Express.js Minimal Web Server Framework Logo

Express is a minimal, unopinionated routing and middleware framework. It simplifies request handling by introducing routing paths and middleware chains:

const express = require("express")
const app = express()
 
app.use(express.json()) // Middleware to parse JSON payloads
 
app.get("/users", (req, res) => {
  res.json([{ id: 1, name: "Wistant Kode" }])
})
 
app.listen(3000)

Why Express Was a Massive Success

  • Simplicity: You can copy-paste a few lines of code and have a server running in seconds.
  • Unopinionated Nature: Express does not care how you structure your folders, connect to your database, validate inputs, or write business logic. It simply receives requests and sends responses.

The Express Limitation: The Spaghetti Code Problem

While Express's unopinionated design works well for small projects or microservices, it presents challenges for larger applications.

Because Express provides no guidance on application structure, developers must make architectural decisions themselves. Without a strict convention, teams often struggle with:

  • Spaghetti Architecture: Controllers, database operations, validation, and business logic are frequently mixed together in single, massive files.
  • Inconsistent Codebases: Two different Express projects built by two different teams can look completely different. Onboarding new developers requires learning a custom, undocumented folder structure.
  • Testing Difficulties: Hard-coded dependencies inside files make it difficult to mock databases or external services, complicating unit testing.

As a result, as an Express application grows, maintaining it and keeping it free of bugs becomes increasingly difficult.


Enter NestJS: Introducing Structure to Node.js

NestJS was created to solve the structural challenges of Express. It does not replace the underlying server libraries; instead, it wraps them in a highly organized, modular architecture.

By default, NestJS uses Express under the hood to handle HTTP routing, but it layers a strict, object-oriented, TypeScript-based architecture on top of it.

Architecture comparison between unopinionated Express and structured NestJS

What NestJS Resolves

  • Strict Conventions: NestJS defines where code should go. Controllers handle requests, Services handle business logic, and Modules group related features.
  • TypeScript First: NestJS is built from the ground up with TypeScript, ensuring type safety and autocomplete support in your IDE.
  • Built-in Dependency Injection: Classes declare their dependencies in their constructors, and NestJS automatically instantiates them, simplifying code organization and testing.

The Three Pillars of NestJS

To build applications with NestJS, you need to understand its three primary building blocks: Controllers, Providers (Services), and Modules.

1. Controllers (Routing and Request Handling)

Controllers are responsible for handling incoming HTTP requests and returning responses to the client. They use TypeScript decorators (annotations starting with @) to map routes to class methods.

import { Body, Controller, Get, Post } from "@nestjs/common"
 
@Controller("users")
export class UsersController {
  @Get()
  findAll(): string[] {
    return ["User A", "User B"]
  }
 
  @Post()
  create(@Body() payload: any): string {
    return `User created successfully`
  }
}
  • @Controller('users') registers this class to handle requests at the /users path.
  • @Get() maps GET requests to the findAll method.
  • @Body() extracts the incoming request payload.

2. Providers and Services (Business Logic)

Providers contain the business logic of your application. The most common provider is a Service, which handles database operations, calculations, or external API requests.

Services are annotated with the @Injectable() decorator, which tells the NestJS framework that this class can be injected as a dependency into other classes.

import { Injectable } from "@nestjs/common"
 
@Injectable()
export class UsersService {
  private readonly users: string[] = ["Wistant Kode", "Developer"]
 
  getAllUsers(): string[] {
    return this.users
  }
 
  addUser(name: string) {
    this.users.push(name)
  }
}

3. Modules (Feature Bundles)

Modules are the packaging mechanism NestJS uses to organize the application. A Module is a class annotated with the @Module() decorator. It groups related Controllers and Providers together.

Every NestJS application has at least one root module (typically AppModule). As your app grows, you create separate feature modules (e.g., UsersModule, AuthModule) to keep the codebase clean.

import { Module } from "@nestjs/common"
 
import { UsersController } from "./users.controller"
import { UsersService } from "./users.service"
 
@Module({
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

Dependency Injection Made Simple

One of the most powerful concepts in NestJS is Dependency Injection (DI).

In standard JavaScript, if your controller needs to use a service, you might instantiate it manually:

// The manual way (Avoid this in NestJS)
class UsersController {
  constructor() {
    this.usersService = new UsersService()
  }
}

This manual approach couples the controller directly to the service class, making it hard to modify or test. In NestJS, you declare the dependency in the constructor parameter, and the NestJS framework injects it automatically:

// The NestJS way
@Controller("users")
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
 
  @Get()
  getUsers() {
    return this.usersService.getAllUsers()
  }
}

When NestJS boots, it resolves the dependency graph:

  1. It sees that UsersController needs UsersService.
  2. It instantiates UsersService first.
  3. It creates UsersController and passes the service instance into its constructor automatically.

This design makes unit testing straightforward because you can pass a mocked version of the service into the controller constructor without running the entire framework database connection stack.


Bootstrapping a NestJS Project

To get started, make sure you have Node.js and a package manager like pnpm installed. You can use the NestJS CLI to scaffold projects:

# Verify Node.js
node --version
 
# Bootstrap a new project using pnpm
pnpm dlx @nestjs/cli new my-nest-app --package-manager pnpm
cd my-nest-app
pnpm run start:dev

Open your browser and visit http://localhost:3000 to see the running application.

Essential CLI Generators

You can use the CLI to generate files rather than creating them manually. The CLI automatically imports and registers the generated classes inside the correct module:

# Generate a controller
nest generate controller products
 
# Generate a service
nest generate service products
 
# Generate a complete CRUD module with controller, service, entities, and tests
nest generate resource products

Exploring the Under-the-Hood Ecosystem

Once you are comfortable with the basics, NestJS provides a rich ecosystem of packages maintained by the official github.com/nestjs organization.

NestJS Platform Support and Ecosystem Integrations

Some of the most useful modules to learn as your skills progress include:

  • @nestjs/config: Manages environment variables and configurations securely.
  • @nestjs/swagger: Generates interactive OpenAPI documentation automatically using TypeScript decorators.
  • @nestjs/common: Houses the middleware, exception filters, pipes, and guards that govern how requests behave.

The Request Lifecycle Sequence

When a request hits a NestJS app, it flows through a structured pipeline:

  1. Middleware: Modifies headers or logs request details.
  2. Guards: Handles authentication and authorization.
  3. Interceptors (Pre-handler): Runs code before the controller (e.g., setup timers).
  4. Pipes: Validates and transforms the request body (e.g., verifying database IDs are strings).
  5. Controller Handler: Executes your business logic.
  6. Interceptors (Post-handler): Modifies the outgoing response.
  7. Exception Filters: Catches and formats any errors before they reach the user.

Swapping the underlying server engine is also straightforward. If you need higher throughput, you can swap out Express for Fastify by installing @nestjs/platform-fastify and updating your bootstrapping configuration inside src/main.ts.


Conclusion

NestJS helps you transition from writing server scripts to designing organized, maintainable software architectures. By providing a structured template out of the box, it allows developers to focus on writing business logic instead of organizing files, helping teams build scalable backend systems.

Command Palette

Search for a command to run...