Inside Nestjs project
Nestjs is an enterprise-grade backend server solution based on Node.js heavily inspired by the Java server and framework Spring. If you know Java EE syntax, you will find many method names familiar.
Project kick-off
To create a new Nestjs project, you need only to run provided CLI and name the project as
shell
Copied!
npx @nestjs/cli new ${APP NAME}
Command will scaffold the full structure of the app, and development can be started. What are the basic building blocks of Nestjs?
Modules
Modules are used to organize the application structure. Every application has the root module and descendant modules which form the hierarchical structure. Another feature of the module is to encapsulate the providers and define which providers should be accessible by other modules. Exported providers can be considered the public interface of the module.
javascript
Copied!
@Module({
imports: [TypeOrmModule.forFeature([Record])],
providers: [RecordsService, SeedingService, Logger],
controllers: [RecordsController],
})
export class RecordsModule {}
Providers
Providers - the module or other modules can usually instantiate services, factories, repositories, or helpers. Providers can inject dependencies, and it is up to Nestjs to compose the dependencies graph at runtime.
Every provider has to be injectable and can be created as a singleton with an application lifetime or scoped to the lifetime of the request. Again, this is a nonstandard pattern helping to cover specific use cases. Providers might hook on multiple application life cycle events as bootstrapping or shut down.
Providers are explicitly the right ones to implement database operations or delegate business logic from the controllers.
javascript
Copied!
@Injectable()
export class SeedingService implements OnApplicationBootstrap {
constructor(
@InjectRepository(Record)
private readonly repository: Repository<Record>,
@Inject(Logger)
private readonly logger: LoggerService,
) {}
async onApplicationBootstrap() { }
}
Controllers
Controllers in the Nestjs are components responsible for handling requests and providing responses for the specified group of requests. Routing defines which controller is used to process the request and which method to apply.
Usually, a single controller takes care of multiple routes and methods. To emphasize the DRY method and to make creating controllers simpler, Nestjs supports annotations.
Controller methods can benefit from a built-in set of standard exceptions, which are automatically mapped to the error responses or extend the set with custom exceptions.
javascript
Copied!
@Controller('vaccination-records')
export class RecordsController implements CrudController<Record> {
constructor(public service: RecordsService) {}
@Get('export')
async exportCsv(@Headers('Accept') accept, @Res() response: Response) {
...
}
}
What are other utility classes?
Oh yes, Nestjs contains multiple components to control the request life cycle with fine graded distinction over them. In some other frameworks, most of the operations are handled in the middlewares. Nestjs provides the right tool for each job. This makes architecture easier to understand and cleaner.
Request life cycle looks as following:
Middlewares
Middlewares are used to modify the request and response before they are passed to the controllers. Middlewares are chained one after another until all of them run, or one ends the cycle.
Middlewares are executed only before the request is passed to the controller in contracts to Interceptors.
The most simple use case for the Middleware is logging.
javascript
Copied!
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
...
next();
}
}
Pipes
Pipes, similarly to middlewares, are used to intercept the requests and apply some changes to them.
The difference is that pipes run in the context of the controller and its route handler. A pipe is running just before the controller method is executed.
Pipes can be constructed even as global and applied to all requests in the application.
Common use cases for pipes are value transformations as parsing or providing default values and validation. Nestjs provides the build-in set of pipes to cover both.
Validation pipes in conjunction with Typescript capabilities allow creating simple and powerful validation using annotations. This allows to attach validation to DTOs and have a single source of truth for every data object.
javascript
Copied!
export class BaseRecordDto implements RecordInterface {
@ApiProperty()
@IsNotEmpty()
@IsString()
@MinLength(2)
@MaxLength(100)
lastName: string;
@ApiProperty()
@IsNumber({ allowInfinity: false })
@Min(18)
age: number;
}
Interceptors
Interceptors are similar to middlewares but can modify both requests before passing them to controllers and response returned from the controller. Interceptors can completely override the controller behavior for a defined route or even skip it.
A common use case for an interceptor is cache or logger. Similar to pipes, interceptors can be scoped to a single controller and method or used globally for the entire application.
javascript
Copied!
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now();
const path = context.switchToHttp().getRequest().route?.path;
console.log(`Request path: ${path}`)
return next
.handle()
.pipe(
tap(() => console.log(`Request took ${Date.now() - now}ms`)),
);
}
}
Guards
Guards are used in Nestjs applications to provide authorization for the controller, which the guard attaches to.
It works similarly to middleware but provides the execution context. Execution context allows accessing request data, controller class, and next handler to be executed. Guards are executed after middlewares and before the pipes.
The denied request will respond with a default error response but can be further caught and refined with specific pipes.
javascript
Copied!
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}
Exception filters
Nestjs application automatically catches all unhandled exceptions and responds tries to categorize them or respond with default internal server error.
This is happening in the default exception filter. Custom exception filters allow extending mapping of the application errors to the user-friendly response errors.
Exception filters have access to the Execution context (same as guards do) to provide rich decision context.
javascript
Copied!
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response
.status(status)
.json({
statusCode: status,
message: "Custom error message",
errorCode: "0011",
});
}
}
Application bootstrapping
Application bootstrapping happens in a specific typescript module called main.ts.
This is a default entry point of the app and defines the global handlers and extends application configuration.
All global components as pipes or providers are defined in the app creation method.
In our example, you can see that we replaced the default application logger with the Winston logger. Added the Swagger module to automatically generate the documentation for the endpoints. At last, there was added the global validation pipe to provide input validation for endpoints.
Nestjs as a powerful tool
As you can see, Nestjs is a mature and potent tool that was built with convention over configuration in mind and is providing a rich set of standard tools. Feel free to see a practical example in our repository, and stay tuned for the next blogs!