I think we all know this problem: You need to develop a new web application and therefore you need to implement the core of the application in the backend and frontend. Both can be time-consuming and if, in the beginning, you choose wrong architectural decisions it can get hardly maintainable over time.
Additionally, projects are often started with a small core team. So it is important that the team provides a solid architecture and is able to provide a good first prototype in time and budget. I, therefore, believe that it could make sense to start with a full stack TypeScript approach where you use Angular in the frontend and NestJS in the backend.
In this article, I will tell you about NestJS (from now on called Nest in this article) and why I think that such a full stack TypeScript web application could be a good tech stack choice for web apps.
Why should I use TypeScript in the backend?
In my opinion, this only makes sense in these scenarios:
- You have a small core team which has a good TypeScript knowledge and this tech stack could fit the needs of your project now and in the future.
- In a project with multiple microservices, you have a specific microservice which serves specifically as a backend for the frontend and is maintained by the frontend team.
If you have an existing backend team which is productive and happy with their tech stack there is, at least in my opinion, no need to use web technologies in the backend. But be at least open for the discussion and maybe it could also fit for your project.
What is Nest?
The official description on the website is:
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with and fully supports TypeScript (yet still enables developers to code in pure JavaScript) and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Reactive Programming).
Under the hood, Nest makes use of robust HTTP Server frameworks like Express (the default) and Fastify. Nest provides a level of abstraction above these frameworks, but can also expose their APIs directly to the developer. This allows for easy use of the myriad third-party modules which are available for each platform.
So basically it uses well-known, existing frameworks like Node.js and Express and acts as a layer above. But why is this now so special?
In my opinion, it is so brilliant due to these facts:
- It is completely written in TypeScript and you can also implement your applications out-of-the-box in TypeScript
- Provides an out-of-the-box application architecture was deeply inspired by Angular
But even if it was inspired by Angular, Nest is a separate project and totally independent of the Angular project itself. You can build a front-end-agnostic API which can be used with other frameworks and libraries like React, Vue.js, and so on.
TypeScript
Nest applications are written in TypeScript but you can also choose to write them in JavaScript (which I would not recommend). TypeScript is a superset of JavaScript and provides the flexibility of JavaScript with the safety and robustness of a typed language.
TypeScript is compiled to JavaScript and therefore the compiler can already catch possible runtime errors during compilation. The language calls itself “JavaScript that scales” and Nest also wants to provide a scalable backend architecture so I think this was a good choice.
Additionally, I would totally recommend you to use TypeScript in the frontend framework you are currently using. Angular provides TypeScript out-of-the-box but you can, of course, also use it in other popular frameworks like React or Vue.
This way you have a common language in your frontend & backend code where you can even share your type definitions. This approach heavily reduces the efficiency losses by context switches from one language to another and it can increase your team performance in general.
Nest Architecture
As already mentioned, Nest’s architecture was deeply inspired by Angular.
Usually, server-side JavaScript frameworks are more focused on flexibility than on providing a scalable architecture for your code. Normally, you need to invest time by yourself to arrange the code in a clear way and define guidelines for it.
Nest provides a nice architecture out of the box which we will now analyze in more details. Basically, it consists of modules, controllers and services.
File structure
A Nest project has a similar file structure than an Angular project:
.
|-- app.controller.spec.ts
|-- app.controller.ts
|-- app.module.ts
|-- app.service.ts
|-- main.ts
|-- news
|-- news.controller.spec.ts
|-- news.controller.ts
|-- news.module.ts
|-- news.service.spec.ts
|-- news.service.ts
Dependency Injection
Nest is built like Angular around the design pattern Dependency Injection. You find a very good article about this pattern in the official Angular documentation.
Let us look at a simple example:
If we need an instance of another class we just need to define it in the constructor:
constructor(private readonly newsService: NewsService) {}
Nest will create and resolve an instance of NewsService
. In the normal case of a singleton, it will return the existing instance if it has already been requested. Every class that can be injected needs to declare the @Injectable
annotation as you can see later in the service section.
Modules
A module is the basic building block of each Nest application and groups related features like services and controllers. If you create a new Nest application you have the AppModule
automatically available.
In theory, you could write your whole application in one module but this is in most of the cases, not the correct approach. It is recommended to group each of your features in a module, for example, a NewsModule
and a UserModule
.
A simple module example:
@Module({
controllers: [NewsController],
providers: [NewsService],
})
export class NewsModule {}
Angular uses the same concept of modules and you even define them the same way in your code.
Controllers
In Nest you use annotations to define your controllers like it is done in frameworks like Spring Boot. Controllers are responsible for handling incoming requests and returning responses to the client.
You decorate your controller class with the required @Controller
decorator which you can pass a path as the primary route for this controller. Each method inside your controller class can be annotated by common decorators like @Get
, @Post
, @Put
, and @Delete
.
@Controller('news')
export class NewsController {
@Get()
findAll(): string {
return 'This action returns all news';
}
}
As we did not add path information to our @Get
decorator of the findAll
method, Nest will map GET /cats
requests to this handler.
Services
Services are used in Nest to keep your controllers slim and encapsulate the logic.
@Injectable()
export class NewsService {
private readonly news: News[] = [{ title: 'My first news' }];
create(news: News) {
this.news.push(news);
}
findAll(): News[] {
return this.news;
}
}
Now we can use this service in our controller:
@Controller('news')
export class NewsController {
constructor(private readonly newsService: NewsService) {}
@Get()
async findAll(): Promise<News[]> {
return this.newsService.findAll();
}
}
Testing
Nest provides us with a setup for unit, integration and end-to-end tests.
Unit Tests
Jest is used as unit test framework in Nest. If you also use the same test framework in the frontend this can be very beneficial and improve your team performance at all.
A simple unit test for our NewsService:
describe('NewsService', () => {
let service: NewsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [NewsService],
}).compile();
service = module.get<NewsService>(NewsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should return first news article', () => {
expect(service.findAll()).toEqual([{ title: 'My first news' }]);
});
});
Thanks to dependency injection it is also very easy to mock services, e.g. in your controller tests:
describe('News Controller', () => {
let controller: NewsController;
const testNews = {
title: 'Test News',
};
class NewsServiceMock {
public findAll() {
return [testNews];
}
}
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [NewsController],
providers: [{ provide: NewsService, useClass: NewsServiceMock }],
}).compile();
controller = module.get<NewsController>(NewsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
it('should get news', () => {
expect(controller.findAll()).toEqual([testNews]);
});
});
End-to-end test
With end-to-end tests you test the whole functionality of your API and not just one particular function.
Nest uses Supertest which can be used to simulate HTTP requests:
describe('News Controller (e2e)', () => {
let app;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [NewsModule],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/news')
.expect(200)
.expect([{ title: 'My first news' }]);
});
});
Swagger
If you develop a public API you may need to use OpenAPI (Swagger) specification to describe RESTful APIs. Nest provides a module to integrate it.
We only need to install the swagger package (@nestjs/swagger
) and add a few lines to our main.ts.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const options = new DocumentBuilder()
.setTitle('News example')
.setDescription('The news API description')
.setVersion('1.0')
.addTag('news')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
In our main.ts we specified that the API doc is available under /api
. So if we open up a browser and navigate to http://localhost:3000/api we get the following API doc.
Getting Started
I can highly recommend the official Nest documentation as a starting point.
Nest provides the Nest CLI which is a very nice command-line interface tool that helps you to initialize and develop your applications. You can scaffold a new project or add new services, component and more as you know it from the Angular CLI.
Basically, you just need these three commands to let the basic project run locally at http://localhost:3000/:
$ npm i -g @nestjs/cli
$ nest new project-name
$ npm run start
The Future of Nest
Nest has currently more than 14k stars on GitHub and more than 100k weekly downloads on npm. It is already used by many companies in production with Adidas as the biggest user.
Nest is unique as server-side JavaScript framework due to its use of TypeScript and relation to Angular but it is missing the support of a large enterprise. This is just a minor concern but it should be taken into account if you choose a framework for your tech stack.
Conclusion
In my opinion, Nest fits perfectly into a full stack Typescript web application tech stack, especially if you choose Angular in the frontend.
The folder structure and design patterns in Nest are heavily based on Angular. Due to this simple and similar structure, we as developers can more focus on the design of the endpoints instead of wasting time for application structuring. Nest hides a lot of the nasty Node.js and ugly JavaScript behind annotations, structures and patterns. Thanks to TypeScript as backend language, going from your frontend to backend code is less painful and without fewer context switches.
Top comments (11)
I imagine this is very exciting for people that like dependency injection, but I have stopped seeing the value in it (especially in TS/JS). I used it for many years in C# and I understand it’s place there. But since Jest and Sinon allow you to do runtime substitution of any object in a test, then is inversion if control really necessary?
I’d very much be interested in your answer becaus I’m nervous that I’m missing something.
Well I can't speak to DI in general, but in angular I will occasionally want to replace injected services with a new class when consuming them in a different environment (e.g. on the server vs the browser). Alternatively, I've had shared services that are extended or modified by individual applications or individual (Angular) components. Angular's DI system makes this easy.
As an example: I have a constellation of apps that rely on the same
UserAccountService
. Most of the apps don't require a user to be logged in, but one app does require users to be logged in. Among other things, the app that requires users to be logged in extends the standardUserAccountService
and overwrites it'sgetCurrentUser()
method so that if the current user isnull
it redirects to the login page. Because of DI, any components callingUserAccountService#getCurrentUser()
, including shared library components, will make use of the replaced service.It took me a while to get comfortable with Angular's DI system, and I certainly did fine before I really understood how to use it so I don't view it as an absolutely necessary feature. Now that I'm comfortable with Angular's DI system however, I find it incredibly useful. I imagine in some cases you could accomplish something similar using file replacements in your build system (e.g. file replacements in webpack). It would be very hard to distribute a public module this way, however, as it would require consumers to fiddle with their build system.
You can avoid the big complicated DI of Angular by just using function DI with native JS.
So instead of:
You can invert control:
Now you can mock those two dependencies by passing in a substitute. No Angular required.
Well
No one is claiming that Angular is required. Any React dev can tell you that Angular is not required LOL.
Deep within a shared module which I don't control, I do not have the ability to pass arguments to
function myController(dependency1, dependency2)
The maintainer of that shared module would need to publicly expose the ability to pass in new dependencies, and at that point you're basically building a DI system. Might as well use one that already exists.Angular's dependency injection (and, based on the examples in this post, nestjs' as well) already looks like your "invert control" example:
Without dependency injection though, you need to manually supply
dependency1
anddependency2
all over the place, as well as manually instantiate classes. Super annoying. Angular analyzes the types and "automatically" / transparently supplies the appropriate input arguments.I'm really not looking for an argument though. I was just trying to answer your question "what am I missing?". Clearly you're happy with the way you're currently doing things. There's no reason to pick up a tool you don't need.
Yup, I’m not arguing, And I do appreciate you trying to help me find what I might be missing. But I’m just hoping to share that Jest, Sinon, and most JS libraries that impliment spies allow you to mock any dependency. So the following might not be necessarily true.
Well, I mean it is 100% true, but since Jest can mock import statements, you don’t necessarily have to invert control to pass a spy in because the module loader does that for IOC for you.
Thank you for your thoughts. If someone would be willing to verify my hypothesis, I would appreciate it. So far I have not needed DI, but there might be circumstances I haven’t encountered.
Again, I enjoyed your article and I appreciate your responses. :)
In my case, I haven't used DI for tests but rather for production code. As a real world example, this angular module for working with cookies allows you to replace the
CookieService
with aCookieBackendService
when rendering your app on the server side. As always with programming, I'm sure there are other ways you could accomplish the same thing.As long as you are productive and your team can handle the code, you do not need to use DI. I just prefer it as I am used to it from using Angular.
Very good article! Do you know anything about @nestjs/swagger support for OpenAPI 3?
PS. The jhipster team/comunnity is already working on NHipster (a JHipster blueprint to generate NestJS applications), take a look here: generator-jhipster-nodejs
Thanks! Unfortunately, I am not informed about OpenAPI 3 support of @nestsjs/swagger but you can check this GitHub thread: github.com/nestjs/swagger/issues/1...
Very well written and good overview of Nest! Thank you!
Hello Michael,
Could you please add something on ORM and the databases respectively. Thank you