Welcome back! In the previous lesson, you set up your NestJS project and got it ready to serve both static files and an API. Now, you are ready to start building the actual API endpoints that users and other programs can interact with.
In NestJS, a controller is a class that handles incoming requests and returns responses to the client. Controllers serve as the main entry point for client-side HTTP requests. They don’t contain business logic themselves but instead act as a middleman — they receive the request, forward it to the appropriate service, and return the response. This separation of concerns is crucial for maintaining clean and testable code. Think of a controller as the part of your application that listens for specific URLs (like /users) and decides what to do when someone visits them. Controllers are a key part of building APIs, and learning how to create them is an important step in your journey.
By the end of this lesson, you will know how to create a simple controller that responds to a GET request with a list of users.
Before we dive in, let’s quickly remind ourselves of the current project structure. You already have a basic NestJS project set up, and your main application module looks like this:
This setup ensures that our app is ready to use additional modules, like the UsersModule that we'll build in this lesson.
Note: In the previous lesson, we didn’t include the UsersModule yet. It’s being introduced now so we can start working with user-specific logic. NestJS uses modules to group related functionality, and adding UsersModule here is the first step toward organizing our project into clear, maintainable feature areas.
Let’s create a controller that will handle requests to /users and return a list of users.
But why do we need a /users endpoint at all in our reading tracker app? Think of the reading tracker as something that stores which books each user is reading. So before we can track reading progress, we need to have the concept of a user. This endpoint will be the foundation for managing user data (e.g., registering, identifying who’s reading what).
In NestJS applications, it's common to create separate controllers for each main entity. For example, while we now have a
/userscontroller, later we may also create/booksor/sessionscontrollers. This keeps your codebase modular — each entity has its own logic, routes, and controller.
Here’s the code for the controller:
Let’s break down what’s happening here:
@Controller('users'): This decorator tells NestJS that this controller will handle requests that start with/users.UsersControllerclass: This is the main class for your controller. It will contain methods that handle different types of requests.constructor() {}: This sets up the constructor which is empty for now. Later we'll pass ourusersServicehere.
For NestJS to know about your new controller, you need to register it in a module. Here’s how the UsersModule looks:
controllers: [UsersController]: This tells NestJS to use yourUsersControllerfor handling requests.providers: [UsersService]: This registers the service, which you’ll use more in future lessons.
This module encapsulates everything related to users — their controller and their logic (the service). Every core domain of your app (like users, books, reviews, etc.) should have its own controller and module. This keeps each entity’s logic isolated, testable, and scalable. For example, in the future you might have a BooksModule and a corresponding BooksController, just like we do with Users. As your application grows, each feature (users, books, sessions, etc.) will likely have its own module to keep things organized. This modular approach is one of NestJS’s core strengths.
By adding your controller to the module, you make sure that NestJS can route requests to it. Finally, by importing UsersModule inside AppModule, we make its functionality available to the whole application. NestJS relies on this modular structure — if you don’t import a module, its controllers and services won’t be recognized by the application. So AppModule acts as the central registry that wires everything together.
We made two small changes to improve the mock frontend preview:
- In
index.html, we added a new button:
This allows you to send a GET request to /users/all directly from the browser. When clicked, it will display the user list returned by your controller.
- In
main.ts, we modified the regex fallback for SPA routing:
This ensures that requests to /users/all still return index.html in the browser, which lets our frontend JavaScript load without getting blocked.
This change is only necessary because we are previewing API results using static HTML. In a real project with React or another SPA frontend, this workaround wouldn't be needed — the frontend would be bundled separately and the route handling would be done by the React router.
To make sure you fully understand how the pieces work together, let’s quickly walk through the flow of a request to /users.
- You send a GET request to
/usersfrom your browser or a tool like Postman. - NestJS matches the route to the
@Controller('users')and sees the@Get()decorator on thefindAll()method. - The
UsersController.findAll()method is called. - It returns a hardcoded array of users.
- NestJS sends this array back as a JSON response to your client.
Later, instead of returning hardcoded data, the
findAll()method will callthis.usersService.findAll()to get real user data. This makes it easier to maintain, test, and expand functionality.
In this lesson, you learned what a controller is and how to create one in NestJS. You saw how to set up a simple UsersController that responds to GET requests at /users with a hardcoded list of users. You also learned how to connect your controller to a module so that NestJS can use it.
Next, you’ll get a chance to practice creating and testing controllers yourself. You’ll build on what you learned here to make your API more useful and interactive. Great job making it this far — let’s keep going!
