NestJS Simplified: A Beginner's Practical Guide to Its Core Architecture
It's been over a year since I started working with NestJS to build server-side applications, and I can confidently say it was one of the best decisions I’ve made. Coming from an Express.js background, the structured approach of NestJS felt a bit overwhelming at first. But once I got the hang of it, I realized how much it simplifies backend development, making applications more scalable and maintainable.
Through my experience I’ve broken down NestJS architecture into six key concepts that form the foundation. In this article I’ll talk about these concepts through a practical example by building a User Module I’ll explain how I use each concept and the order in which I apply them when creating a module in NestJS so you can follow along and build with confidence
The Core Architecture of NestJS
When I started with NestJS, the biggest shift from Express.js was its structured architecture. Unlike Express, where you have complete freedom to organize files, NestJS enforces a clear structure that makes applications scalable and maintainable. While this structure may feel overwhelming, it reduces complexity and improves development efficiency.
NestJS is inspired by Angular’s modular approach and introduces concepts like Modules, Controllers, Services, Data Access Object (DAO), Data Transfer Object (DTO), and Schemas. These patterns help write clean, reusable, and testable code. In my experience, these patterns make development much smoother, especially when working on large-scale applications or microservices.
In this section, I’ll break down the core architecture of NestJS so you can see how everything connects.
The first thing you will, obviously, require is a Module. So we will being with implementing a Module.
Modules
Modules are a fundamental part of NestJS because they provide a way to organize related components, such as controllers, providers, and services, into a single unit. This helps keep the application structured, maintainable, and scalable by ensuring that related business logic is grouped together and is not being mixed with other components of the application
The User Module
To explain things practically, a user module is the easiest and most relatable module, as almost all applications include a basic user module.
How a Module is generated?
When you create a NestJS app, it always includes an App Module, which acts as the main entry point for the application and manages all other modules.
To create the Module in NestJS, there are are 2 ways either you manually create all the required files or using the Nest CLI
I prefer using the Nest CLI to create modules because it saves time, avoids boilerplate, and keeps the structure consistent. It also reduces errors and speeds up development.
Run the below command:
nest g module user
Structure of the User Module
Once generated, the User Module will generally consist of the following core files:
The module file is where you organize and connect everything. Typically, it includes:
Below is how the User Module looks in practice:
In this example:
This is how the User Module structure looks when using NestJS, and you can follow this same pattern for any other module you create.
Add User Module to the App Module (Manually)
After creating the UserModule, you need to register it in the AppModule. This step is important because NestJS only recognizes and loads modules that are explicitly imported into the main module tree.
If you created the module manually, you'll have to add this import yourself. But if you generated the module using the Nest CLI, it automatically updates the AppModule for you.
After setting up and configuring the module, the next thing I do is create a basic Data Transfer Object (DTO) because they provide a structure how will my schema look like and what all variables are going to be included, gives you a reference point for better understanding to your schema's structure.
Data Transfer Object (DTO)
DTO is not just a concept of Nestjs but it is a design pattern that is commonly used in software development to transfer data between different layers of an application.
To put in simpler terms a DTO is an object that defines how data will be sent over the network. It acts as a contract between the client and the server, ensuring that only the necessary data is transmitted and validated.
Why Use DTO?
How to create a DTO
In NestJS, DTOs are implemented using TypeScript classes. TO start working with DTOs ensure you have the necessary packages installed:
npm install class-validator class-transformer
The use of above packages enhances DTOs with validation capabilities.
You can place a DTO anywhere in your project, but a good practice is to keep it inside the module it belongs to. Since we’re working on the User Module, all user-related DTOs should be stored within the user module.
Here, we’ve used decorators from class-validator to enforce validation rules:
Implementing a DTO in a Controller
To understand how a DTO can be practically used in a use-case, we can take controller for an example, Controllers will be discussed in detailed.
A common practice followed to use a DTO is in controllers, where a request is received. A DTO will sanitize the incoming data and then forward it to the service.
Once the DTO is in place and I know what data I'm working with the next step is to define the Schema This is where I actually shape how that data will be stored and handled in the database
Schemas
After setting up the DTO the next step is to define the Schema, this is where it is decide how the data will be stored in the database. For the user module it means outlining what a user document should look like including the fields it should have and how each one should behave.
One helpful practice I follow is extending the schema class with the DTO I defined earlier This inheritance ensures that all the fields I planned for are included in the schema keeping everything in sync and reducing the chance of missing anything.
What and Why?
A schema in NestJS is used to define the structure of documents that will be stored in database's collections. While the DTO helps structure incoming data the schema ensures that data is stored in a reliable and consistent way.
For our User Module a document in the schema can contain fields like name, email, and phone number along with their types and validation rules.
How to create a Schema
In the user's module itself you can create a file for schema as well, where you will define the schema class.
Below is a basic example of how to implement a schema:
Here’s what’s happening in the above code:
@Schema({ collection: 'custom_users' })
This will use custom_users as the collection name instead of the default.
A logical next step after defining the schema is determining how to access the collection. While there are various approaches, I prefer implementing a Data Access Object (DAO) layer.
Data Access Object (DAO)
In backend development, one of the core responsibilities is interacting with the database, in order to do that you can add layer between service and the database called Data Access Object (DAO).
In NestJS, a DAO is a class specifically responsible for performing database operations like CRUD and you write your own custom queries in this class as well. It serves as an abstraction layer, isolating direct database access from the service layer.
Each module typically has its own DAO class that handles all database interactions for that module only. For example, the UserDAO in a UserModule is solely responsible for managing the users collection.
Why DAO?
Before understanding why, is DAO even needed? No
In fact, official NestJS documentation does not explicitly mention or suggest the use of DAO. Instead, it emphasizes integrating with various ORM tools and database libraries, such as TypeORM, Sequelize, and Mongoose, to manage database interactions.
Developers who come from Express.js, where the convention is to directly interact a service with the database, background always consider adding DAO as an extra step.
However, as your application grows, you’ll quickly realize the benefits of this pattern.
Here are the key reasons why using a DAO layer is a smart choice:
Creating DAO
As DAO is something we add to handle database operations smoothly therefore there isn't any command provided by Nest CLI to generate it. We need to create these files manually and then inject them where intended.
For practical demonstration we will continue to extend our User module and add the User DAO Class in the module
Step 1: Create the UserDAO Class
To begin, create a user.dao.ts file in the user module folder. Below is how the UserDAO class might look:
Step 2: Inject DAO into Service
To make the DAO class available to your service you can just inject it in your UserService class by importing it in the file.
Step 3: Register DAO Module
Finally, you must register your DAO in the User Module so that NestJS can manage it as a provider and make it available to be used by any other class in the module. Without registering it, NestJS wouldn’t know how to instantiate or inject the DAO when needed.
With the DAO now handling all database operations for the User module, the next step is to connect this layer to the core business logic. That’s where the Service Layer comes in, acting as the bridge between our application’s features and the data managed by the DAO.
Service
Once a request reaches a controller, the next step is to process that request using the application's business logic, and that’s where services come in. Services are typically used by controllers to perform various operations, such as querying a database, processing data, or interacting with external APIs.
In our User Module, the service will sit between the controller and the DAO, receiving input from the controller, applying any business rules, and delegating database actions to the DAO.
Creating and Using the Service
To create a service, we can use the CLI command below, the command will create the service inside the User Module.
nest generate service user
In the user module, you will find two files:
The UserService class is automatically decorated with @Injectable() and is ready to be used in your module.
UserService Implementation
Explanation of Service class:
With the service layer in place, the core business logic and data flow between the DAO and the rest of the application is well-handled. The last layer that brings everything together is the Controller, responsible for handling incoming requests and connecting them to the right service methods.
Controllers
A controller is responsible for handling incoming requests, it acts as the entry point for the application’s routes. Each controller is linked to a specific route path and uses decorators like @Get(), @Post(), etc., to define how it responds to different types of requests.
The main purpose of a controller is to help navigate a request to its intended service it does not contain business logic, instead, they delegate tasks to services.
How to Create a Controller
You can create a controller manually or use the Nest CLI. The CLI is recommended as it sets up everything for you.
Since we've already created a UserModule, the next step is to define the UserController.
nest g controller user
This command generates user.controller.ts and optionally a test file user.controller.spec.ts.
Structure of a Controller
A typical controller uses these decorators:
Routing with Controllers
In NestJS, routing is defined using decorators. These decorators are special functions prefixed with @, and they provide metadata to the framework, helping it understand how different routes should behave within your application.
Each route corresponds to a method in your controller, and you can use decorators like @Get(), @Post(), @Put(), and @Delete() to handle different types of HTTP requests.
Let’s extend our UserController to demonstrate how routing works with different HTTP methods:
By the end of setting up the controller, the module is completed with essentials, routes defined, services connected, and data flowing properly. This layer ties everything together and makes the module ready to handle real requests.
Ending Note
Looking back at my journey with NestJS, understanding these core building blocks, Modules, Controllers, Services, DTOs, Schemas, and DAOs, has made a huge difference in how I approach backend development.
At first, the structure felt like a lot to take in, but once I started using them together in real projects, everything started to make sense. These concepts aren’t just theory, they shape how I build and scale features every day.
I’ve shared how I use them, through my experience in building backend systems, step by step in a real module so you can see how they come together in practice. If you're just starting out or trying to structure your code better, I hope this breakdown gives you a solid starting point the way it once did for me.
Backend & Cloud Engineer | Building High-Performance Systems in Fintech & HealthTech | Python, Node.js, Kubernetes, AWS
3moWell put, Ahmed