Intro
I recently had to learn NestJS to get ready for a project that was written on it. Not NextJS, mind you, but NestJS. Like some of you, I’ve heard about it, but never had a chance to actually use it. It has been growing in popularity for quite a while now, catching up with the grandfather of NodeJS back-end development — ExpressJS just earlier this year. Here is their official tweet:
That being said, there is a big discrepancy on npm in terms of pure downloads — Express stands tall at 32,397,019 weekly downloads at the time of writing, Next is at 7,263,688, and Nest is at 3,579,676. Now Nest really is tied to Express to some extent, which will be mentioned in the article.
I want to highlight that NestJS has excellent documentation (filled with cats as a bonus, but only on desktop…) that provides more than enough information to get you started and explain all of its basic and advanced use cases. Always a great place to start!
If you are like me, after an N number of frameworks and languages, the easiest way to learn a new one is to compare and match it to the things you know. Because of that, I thought a comparison between NestJS and the more popular/traditional/established JS frameworks could be useful to understand some of the concepts.
Context
Express.js is great for lightweight, flexible APIs or small projects with minimal restrictions.
Next.js is ideal for React-based apps where SEO and server-side rendering are crucial.
NestJS excels in large-scale, enterprise-level applications with a focus on modularity and maintainability. It is also by far the most opinionated. A lot of those opinions are coming from other languages that some of the more experienced (to avoid saying older devs) are familiar with — inversion of control, dependency injection — Java + Spring, .NET Core, Angular (the main inspiration for NestJS), PHP + Laravel or Symfony, etc.
Let’s start with some basic simple examples and work our way up.
Starting a Simple Server
And finally NextJS -> next dev
In NestJS, the server is bootstrapped by creating an application using NestFactory and specifying the main application module. The structure is more organized with AppModule at the core.
ExpressJS is more straightforward, creating the server directly by invoking express().
Next.js doesn't require manually setting up a server unless you're using a custom server—its built-in server handles everything under the hood.
2. Handling a Route
Where most of your work lies.
NestJS uses the @Controller and @Get decorators to map routes to controller methods, offering more structure with modular routing. Get used to the decorators and annotations for NestJS. They are a critical part of the language (same as Java + Spring for instance).
ExpressJS allows you to define routes freely using app.get(), which provides flexibility but less built-in structure.
And in Next.js, routing is file-based, where the file name in the pages directory automatically maps to a route, giving you simplicity but less control over route definition.
3. Handling Query Parameters
ExpressJS relies on req.query to access query parameters, offering a more manual approach.
Next.js uses the useRouter hook to retrieve query parameters within the component, aligning more with React conventions.
NestJS uses the @Query() decorator to easily extract query parameters in a controller method.
4. Handling Path Parameters
In ExpressJS, path parameters are defined in the route itself using :paramName. For example, app.get('/users/:id') defines the id as a dynamic parameter. You can then access this parameter using req.params.paramName, which is flexible but less structured than NestJS.
In Next.js, path parameters are handled using dynamic routes. To define a path parameter in Next.js, you create a file with square brackets, such as [id].js, inside the pages directory. The file name ([id]) is used to create a route that dynamically matches any value passed as id. You then use the useRouter() hook to access the parameter within the component. This approach is simple and React-friendly but not as explicit as the NestJS approach.
In NestJS, path parameters are defined using the @param() decorator in combination with the @Get(':id') decorator. The @Get(':id') specifies that the id is dynamic, and @param('id') is used to extract the parameter in the controller. Path parameters in NestJS are explicitly defined and tied to controller methods, making them highly structured.
5. Adding Middleware
With a lot of the basics out of the way, let’s get to something more interesting. Middleware is used for various things — checking permissions, validations, logging, and much more. While in Express it is just middleware, in NestJS several different things are doing similar work.
Next.js doesn’t have built-in middleware support in the same way that NestJS and Express do. Middleware functions are generally implemented in custom servers (like Express).
Notice how both Express and Nest have a very similar structure. It is time to share the fact that Nest actually uses Express or Fastify under the hood.
In ExpressJS, middleware is more flexible and can be applied globally using app.use() or scoped to specific routes by passing the middleware function as a second argument in route definitions. Express is more unopinionated, giving developers freedom in how middleware is structured.
NestJS offers a highly structured and modular way to handle middleware, integrating it into its dependency injection system, which is ideal for large applications requiring reusable middleware logic.
6. Dependency Injection
This is one of the more opinionated parts of NestJS when compared to the other two frameworks.
NestJS heavily relies on Dependency Injection (DI), which is a core part of its architecture. You can inject services using the @Injectable decorator. Meanwhile, neither Express nor Next support it out of the box, so typically, you’d pass dependencies manually.
The dependency injection is one of the core design patterns used NestJS. It fits into it the following way:
NestJS uses an IoC container to manage dependency lifecycles, automatically injecting dependencies where needed. This shifts the responsibility of creating and managing dependencies from the developer to the framework, streamlining the setup, and reducing manual effort when wiring services, controllers, and other components.
With DI, NestJS enforces a consistent pattern across the application, ensuring that all dependencies are handled the same way. This makes the architecture predictable and reduces confusion.
As your application grows, the IoC container in NestJS efficiently handles an increasing number of dependencies, ensuring smooth scaling. This prevents the chaos of manually managing complex service dependencies, making the codebase more maintainable and reducing the likelihood of errors.
DI in NestJS encourages loose coupling by separating services and their dependencies, making it easier to modify or replace parts of the system without breaking the entire application. This decoupling also enables easier testing and mocking of service.
While all of these points are great for large-scale applications, the DI pattern can be a complication for smaller projects, especially if you are not used to it.
7. Inversion of control
NestJS automates dependency management using an IoC container, while ExpressJS and Next.js rely on manual instantiation of dependencies. Notice how we are just saying that userService or userRepository are of a specific type. After that, all of the additional handling is taken care of by NestJS. We don’t have to think about when to spawn those components, how many instances of them will be there, or anything else. We do have control over those of course.
This makes the last example much more scalable, modular, and easier to test.
The obvious cons to this approach are — the increased learning curve. This is true for all opinionated frameworks, as well as that it might feel like an overkill for small applications.
8 — Permissions Management
While we are on the subject of differences between NestJS and the other two frameworks, I think it is time to mention Guards. Guards are a core feature for managing permissions. They provide a clean, scalable way to control access based on user roles or any other condition. Guards are highly reusable and can be applied to specific routes or globally across the application.
Express and Next are relying on middleware or manual checks on the session, which makes it easier for small apps, but grows in complexity for the larger the application scales up.
9— Data transformation — Pipes
Similar to how Guards work, Pipes are NestJS specific feature. Their main goal in this example is to manipulate or transform the data before passing it onto the controller.
While the same can be achieved in ExpressJS and NextJS with middleware, using Pipes provides for better error handling and easier maintenance at scale.
Not to mention you have the freedom to create your own custom pipes.
10 — Data validation — Pipes pt.2
Notice that while the annotations/decorators can be odd at first, depending on where you are coming from, the larger the code grows, some things are actually easier to maintain and read. Personal opinion, but while the example in NestJS is longer by a few lines, each line is much shorter and easier to follow.
Additionally, it is once again highlighting how things are more maintainable at scale with NestJS.
You can check NestJS’s official docs to see a longer version of this example with additional explanations. It involves a longer explanation than what I am aiming for in this article, so I’ll just be linking it.
Thank you for making it to the end! I was aiming for a few examples between these mostly recognized frameworks and NestJS. This is the first article in a short series I’ll be making on NestJS and trying to tie in its concepts to more common frameworks as a way to illustrate and make it more easy to remember and understand.
If you have gotten this far, I thank you and I hope it was useful to you! Here is a cool image of a cat as a thank you!
Top comments (1)
good