Chassis for a REST API using NestJS, Express.js, MongoDB and Redis. Tests are run using Jest.
node
v22.1.0npm
v10.7.0- Nest CLI
- MongoDB running locally
- use
node --run start:dev
to run the service in development mode (withNODE_ENV=dev
). - use
node --run lint
for code linting. - use
node --run test
for executing tests. - use
node --run test:e2e
for executing e2e tests. - use
node --run test:cov
for tests coverage.
I also recommend to use ncu
to find outdated dependencies (and run ncu -u
to upgrade package.json
).
App is launched listening on 8080 port by default, set the environment variable PORT to change it.
-
Create NestJS project:
npm i -g @nestjs/cli nest new chassis-nest cd chassis-nest
-
Add Node.js and npm versions used to
package.json
:"engines": { "node": ">=22.1.0", "npm": ">=10.7.0" }
-
Install mongoose:
npm install mongoose
. -
Install dev dependencies such as linter ones (eslint-plugin-json-format):
npm install --save-dev eslint-plugin-json-format
-
Check the eslint configuration,
.eslintrc.json
file should have:"env": { "node": true, "jest": true }
Also add
json-format
plugin (the one installed with the dependencyeslint-plugin-json-format
)"plugins": [ "json-format" ]
Add
.eslintignore
file. -
Create npm configuration file
.npmrc
withengine-strict=true
in order to notify with an error alert when trying to install/test/start something without the correct Node.js and npm versions. -
Install Husky to execute linter fixes and check tests before a commit is created or pushed:
npm install --save-dev husky
. Install husky git hooks (only once):npx husky init
and add it topackage.json
script calledprepare
. If you want to make a commit skipping husky pre-commit git hooks you can usegit commit -m "..." -n
; the same occurs when you want to skip pre-push hooks:git push --no-verify
. -
Install
lint-staged
to check linting only in staged files before making a commit:npm install --save-dev lint-staged
. Add configuration file.lintstagedrc
. -
Install CommitLint dev dependencies to apply Conventional Commits:
npm install --save-dev @commitlint/cli @commitlint/config-conventional
and create its configuration file.commitlintrc.json
:{ "extends": [ "@commitlint/config-conventional" ] }
-
Add
pre-commit
,pre-push
andpre-commit-msg
scripts to be run with husky git hooks:"pre-commit": "npx lint-staged", "pre-commit-msg": "npx --no -- commitlint --edit ${1}", "pre-push": "npx jest",
-
Create
.husky/pre-commit
file to insert command that should be executed before making a commit. This file looks like this:node --run pre-commit
-
Create
.husky/pre-commit-msg
file to insert command that should be executed to check the commit message. This file looks like this:node --run pre-commit-msg
-
Create
.husky/pre-push
file to insert command that should be executed before pushing a commit. This file looks like this:node --run pre-push
If tests fail, commit won't be pushed.
-
Create a new controller:
nest g co
. -
Create a service:
nest g s
. -
Create a module:
nest g mo
. -
Generate a DTO class (with --no-spec because no test file needed for DTO's):
nest g class cats/dto/create-cat.dto --no-spec
nest g class cats/dto/update-cat.dto --no-spec
. -
Install DTO validator dependencies (in order to send a 400 BAD_REQUEST error when body does not match DTO):
npm i class-validator class-transformer
npm i @nestjs/mapped-types
-
Apply the
ValidationPipe
globally in ourmain.ts
file:app.useGlobalPipes(new ValidationPipe({ whitelist: true, // avoid creating/updating objects with fields not included in DTO by ignoring them forbidNonWhitelisted: true, // avoid creating/updating objects with fields not included in DTO by throwing an error 400 transform: true, // enable auto transform feature to transform body in an instance of the proper DTO, or path/query params to booleans or numbers depending on the indicated type in the controller }));
-
Implement validation rules in our
CreateCatDto
(i.e.@IsString()
). -
Import validation rules in
UpdateCatDto
fromCreateCatDto
and set attributes to be optional (no more duplicated code!). -
Install mongoose dependencies:
npm i mongoose @nestjs/mongoose
. -
Setup
MongooseModule
inAppModule
:@Module({ imports: [ MongooseModule.forRoot('mongodb://localhost:27017/nest-course'), ], }) export class AppModule {}
-
Migrate Cat entity to a Mongoose Schema using
@nestjs/mongoose
(collection name will be Cats by default if class is named Cat). -
Add Schema to
MongooseModule
:@Module({ imports: [ MongooseModule.forFeature([ { name: Cat.name, // the name of the Cat typescript class, which is "cat" schema: CatSchema } ]) ], controllers: [CatsController], providers: [CatsService], }) export class CatsModule {}
-
Use Mongoose Cat Model in
cats.service.ts
:constructor( @InjectModel(Cat.name) private catModel: Model<Cat>, ) {}
-
Install
@nestjs/config
to work with environment variables in process.env:npm i @nestjs/config
. UpdateAppModule
to use process.env variables:import { ConfigModule } from '@nestjs/config' @Module({ imports: [ ConfigModule.forRoot(), // load and parse our .env file from default location ... ], }) export class AppModule {}
To specify another path for this file, let’s pass in an options object into the
forRoot()
method and set theenvFilePath
property like so:ConfigModule.forRoot({ envFilePath: `.env${process.env.NODE_ENV ? `.${process.env.NODE_ENV}` : ''}`, })
-
Generate exception filter to catch exceptions:
nest g filter middlewares/http-exception
. Addapp.useGlobalFilters(new HttpExceptionFilter());
inmain.ts
. -
Create authentication guard:
nest g guard auth
and install dependencies needed for authentication process:npm i jsonwebtoken uuid ioredis
. Create authentication service and its dynamic module. Create cache service and its dynamic module. Import both modules inAppModule
and set guard inmain.ts
:// app.module.ts @Module({ imports: [ ... CacheModule.forRootAsync({ useFactory: () => ({ uri: process.env.REDIS_URI, }), }), AuthModule.forRoot({ jwtSecret: process.env.JWT_SECRET, uuidNamespace: process.env.UUID_NAMESPACE, }), ], }) // main.ts const authService = app.get(AuthService); app.useGlobalGuards(new AuthGuard(authService));
Another option (instead of using
app.useGlobalGuards
) is addingAuthGuard
as a provider inAuthModule
:import { APP_GUARD } from '@nestjs/core'; @Module({ providers: [ { provide: APP_GUARD, useClass: AuthGuard }, ... ], ... }) export class AuthModule {}
-
Create
JwtUser
decorator (to retrievereq.jwtUser
) and use it like this:@JwtUser() jwtUser
.export const JwtUser = createParamDecorator( (_data: unknown, ctx: ExecutionContext) => { const req = ctx.switchToHttp().getRequest(); return req.jwtUser; }, );
-
Document API with OpenAPI Specification.
18.1. Install
@nestjs/swagger
and Swagger UI for Express.js:npm i @nestjs/swagger swagger-ui-express
.18.2. Set up Swagger document in
main.ts
:import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; const options = new DocumentBuilder() .setTitle('chassis-nest') .setDescription('Chassis for a REST API using NestJS, Express.js, MongoDB and Redis') .setVersion('1.0') .build(); const document = SwaggerModule.createDocument(app, options); SwaggerModule.setup('openapi', app, document);
To view the Swagger UI go to:
http://localhost:8080/openapi
.18.3. Having a DTO of a POST/PUT request body is not enough to automatically generate de OpenAPI schemas out of the box. This can be addressed at compilation time. Nest provides a plugin that enhances the TypeScript compilation process to reduce the amount of boilerplate code we'd be required to create:
@nestjs/swagger/plugin
. We can enable NestJS Swagger plugin to help automate the documentation process. Add@nestjs/swagger/plugin
to our applicationnest-cli.json
to enable the Swagger CLI plugin:"compilerOptions": { "deleteOutDir": true, "plugins": ["@nestjs/swagger/plugin"] }
18.4. We can now use decorators to set (or override) API methods/properties/responses documentation:
@ApiProperty
,@ApiResponse
,@ApiTags
... We also need to fix PartialType for Swaggerimport { PartialType } from '@nestjs/mapped-types';
➡️import { PartialType } from '@nestjs/swagger'
; -
Create
entity.repository.ts
andcats.repository.ts
to follow de Repository Pattern. -
Create cache interceptor:
nest g interceptor common/interceptors/cache
(we cannot use a cache middleware because middlewares are executed before guards, and we needAuthGuard
to be runned before the cache interceptor in order to authenticate the user previously). Addapp.useGlobalInterceptors(new CacheInterceptor(cacheService));
inmain.ts
. -
Install
morgan
:npm i morgan
. Create logger middleware:nest g middleware common/middlewares/http-request-logger
. Configure it inAppModule
:@Module({ ... }) export class AppModule { configure(consumer: MiddlewareConsumer) { process.env.NODE_ENV !== 'test' && consumer .apply(HttpRequestLoggerMiddleware) // do not log this call, too much flood .exclude('openapi') .forRoutes('*'); } }
-
Add
Dockerfile
and.dockerignore
. After that, you can create de docker image and run the docker container with the following commands:docker build -t [IMAGE_NAME] . docker run --name [CONTAINER_NAME] -p 8080:8080 -t -d [IMAGE_NAME]
-
Configure GitHub Action in
.github/workflows/main.yaml
. This action executes linter and tests and reads the GitHub secrets of the repository to fill the .env file with the secret calledENV_FILE
and use theGITHUB_TOKEN
secret to build and push a Docker image to GitHub Packages. -
Use
zod
instead ofclass-validator
. Thanks to @juliojordan ❤️ Zod demo and NestJS documentation.