This is starter of a Nest.js 10 application with a MongoDB replica set + Prisma ODM.
- JWT Authentication
- CASL Integration
- Simple query builder
- Data Pagination
- Data Sorting
- Data Filtering
- Exception Filters
- Validation Pipes
- Swagger Documentation
- Docker Compose
- MongoDB Replica Set
- Serializers
- Health Check
- SWC (Speedy Web Compiler)
- Prisma
- Twilio
- AWS S3
- AWS SQS
- Nest.js 10
- Docker
- Docker Compose
- MongoDB
- Node.js
- NPM
- Create volume for each MongoDB node
docker volume create --name mongodb_repl_data1 -d local
docker volume create --name mongodb_repl_data2 -d local
docker volume create --name mongodb_repl_data3 -d local
- Start the Docker containers using docker-compose
docker-compose up -d
- Start an interactive MongoDb shell session on the primary node
docker exec -it mongo0 mongosh --port 30000
# in the shell
config={"_id":"rs0","members":[{"_id":0,"host":"mongo0:30000"},{"_id":1,"host":"mongo1:30001"},{"_id":2,"host":"mongo2:30002"}]}
rs.initiate(config);
4 Update hosts file
sudo nano /etc/hosts
# write in the file
127.0.0.1 mongo0 mongo1 mongo2
- Connect to MongoDB and check the status of the replica set
mongosh "mongodb://localhost:30000,localhost:30001,localhost:30002/?replicaSet=rs0"
- Run migrations
npm run db:migrate:up
Need to apply migration
token-ttl-indexes
to database This migration create TTL indexes forrefreshToken
andaccessToken
fields inTokenWhiteList
model. Token will automatically deleted from database when token expriration date will come.
- Install dependencies
npm install
- Generate Prisma Types
npm run db:generate
- Push MongoDB Schema
npm run db:push
- Start the application
npm run start:dev
By default SWC is used for TypeScript compilation, but it can be changed. To use tsc
as project builder, change Nest CLI config:
// nest-cli.json
{
...,
"compilerOptions": {
...,
"builder": "tsc" // type "swc" to return back to SWC
}
}
And change Jest config for tests:
// jest-e2e.json
{
...,
"transform": {
"^.+\\.(t|j)s?$": ["ts-jest"] // replace with "@swc/jest" to return back to SWC
},
}
Pagination is available for all endpoints that return an array of objects. The default page size is 10. You can change the default page size by setting the DEFAULT_PAGE_SIZE
environment variable.
We are using the nestjs-prisma-pagination library for pagination.
Example of a paginated response:
{
data: T[],
meta: {
total: number,
lastPage: number,
currentPage: number,
perPage: number,
prev: number | null,
next: number | null,
},
}
The query builder is available for all endpoints that return an array of objects. You can use the query builder to filter, sort, and paginate the results. We are using the nestjs-pipes library for the query builder.
Example of a query builder request:
GET /user/?where=firstName:John
@Get()
@ApiQuery({ name: 'where', required: false, type: 'string' })
@ApiQuery({ name: 'orderBy', required: false, type: 'string' })
@UseGuards(AccessGuard)
@Serialize(UserBaseEntity)
@UseAbility(Actions.read, TokensEntity)
findAll(
@Query('where', WherePipe) where?: Prisma.UserWhereInput,
@Query('orderBy', OrderByPipe) orderBy?: Prisma.UserOrderByWithRelationInput,
): Promise<PaginatorTypes.PaginatedResult<User>> {
return this.userService.findAll(where, orderBy);
}
Swagger documentation is available at http://localhost:3000/docs
By default, AuthGuard
will look for a JWT in the Authorization
header with the scheme Bearer
. You can customize this behavior by passing an options object to the AuthGuard
decorator.
All routes that are protected by the AuthGuard
decorator will require a valid JWT token in the Authorization
header of the incoming request.
// app.module.ts
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
]
You can skip authentication for a route by using the SkipAuth
decorator.
// app.controller.ts
@SkipAuth()
@Get()
async findAll() {
return await this.appService.findAll();
}
Define roles for app:
// app.roles.ts
export enum Roles {
admin = 'admin',
customer = 'customer',
}
nest-casl
comes with a set of default actions, aligned with Nestjs Query.
manage
has a special meaning of any action.
DefaultActions aliased to Actions
for convenicence.
export enum DefaultActions {
read = 'read',
aggregate = 'aggregate',
create = 'create',
update = 'update',
delete = 'delete',
manage = 'manage',
}
In case you need custom actions either extend DefaultActions or just copy and update, if extending typescript enum looks too tricky.
Permissions defined per module. everyone
permissions applied to every user, it has every
alias for every({ user, can })
be more readable. Roles can be extended with previously defined roles.
// post.permissions.ts
import { Permissions, Actions } from 'nest-casl';
import { InferSubjects } from '@casl/ability';
import { Roles } from '../app.roles';
import { Post } from './dtos/post.dto';
import { Comment } from './dtos/comment.dto';
export type Subjects = InferSubjects<typeof Post, typeof Comment>;
export const permissions: Permissions<Roles, Subjects, Actions> = {
everyone({ can }) {
can(Actions.read, Post);
can(Actions.create, Post);
},
customer({ user, can }) {
can(Actions.update, Post, { userId: user.id });
},
operator({ can, cannot, extend }) {
extend(Roles.customer);
can(Actions.manage, PostCategory);
can(Actions.manage, Post);
cannot(Actions.delete, Post);
},
};
// post.module.ts
import { Module } from '@nestjs/common';
import { CaslModule } from 'nest-cast';
import { permissions } from './post.permissions';
@Module({
imports: [CaslModule.forFeature({ permissions })],
})
export class PostModule {}
CaslUser decorator provides access to lazy loaded user, obtained from request or user hook and cached on request object.
@UseGuards(AuthGuard, AccessGuard)
@UseAbility(Actions.update, Post)
async updatePostConditionParamNoHook(
@Args('input') input: UpdatePostInput,
@CaslUser() userProxy: UserProxy<User>
) {
const user = await userProxy.get();
}
Sometimes permission conditions require more info on user than exists on request.user
User hook called after getUserFromRequest
only for abilities with conditions. Similar to subject hook, it can be class or tuple.
Despite UserHook is configured on application level, it is executed in context of modules under authorization. To avoid importing user service to each module, consider making user module global.
// user.hook.ts
import { Injectable } from '@nestjs/common';
import { UserBeforeFilterHook } from 'nest-casl';
import { UserService } from './user.service';
import { User } from './dtos/user.dto';
@Injectable()
export class UserHook implements UserBeforeFilterHook<User> {
constructor(readonly userService: UserService) {}
async run(user: User) {
return {
...user,
...(await this.userService.findById(user.id)),
};
}
}
//app.module.ts
import { Module } from '@nestjs/common';
import { CaslModule } from 'nest-casl';
@Module({
imports: [
CaslModule.forRoot({
getUserFromRequest: (request) => request.user,
getUserHook: UserHook,
}),
],
})
export class AppModule {}
or with dynamic module initialization
//app.module.ts
import { Module } from '@nestjs/common';
import { CaslModule } from 'nest-casl';
@Module({
imports: [
CaslModule.forRootAsync({
useFactory: async (service: SomeCoolService) => {
const isOk = await service.doSomething();
return {
getUserFromRequest: () => {
if (isOk) {
return request.user;
}
},
};
},
inject: [SomeCoolService],
}),
],
})
export class AppModule {}
or with tuple hook
//app.module.ts
import { Module } from '@nestjs/common';
import { CaslModule } from 'nest-casl';
@Module({
imports: [
CaslModule.forRoot({
getUserFromRequest: (request) => request.user,
getUserHook: [
UserService,
async (service: UserService, user) => {
return service.findById(user.id);
},
],
}),
],
})
export class AppModule {}
Extending enums is a bit tricky in TypeScript There are multiple solutions described in this issue but this one is the simplest:
enum CustomActions {
feature = 'feature',
}
export type Actions = DefaultActions | CustomActions;
export const Actions = { ...DefaultActions, ...CustomActions };
For example, if you have User with numeric id and current user assigned to request.loggedInUser
class User implements AuthorizableUser<Roles, number> {
id: number;
roles: Array<Roles>;
}
interface CustomAuthorizableRequest {
loggedInUser: User;
}
@Module({
imports: [
CaslModule.forRoot<Roles, User, CustomAuthorizableRequest>({
superuserRole: Roles.admin,
getUserFromRequest(request) {
return request.loggedInUser;
},
getUserHook: [
UserService,
async (service: UserService, user) => {
return service.findById(user.id);
},
],
}),
// ...
],
})
export class AppModule {}
PrismaModule
provides a forRoot(...)
and forRootAsync(..)
method. They accept an option object of PrismaModuleOptions
for the PrismaService and PrismaClient.
If true
, registers PrismaModule
as a global module. PrismaService
will be available everywhere.
import { Module } from '@nestjs/common';
import { PrismaModule } from 'nestjs-prisma';
@Module({
imports: [
PrismaModule.forRoot({
isGlobal: true,
}),
],
})
export class AppModule {}
If true
, PrismaClient
explicitly creates a connection pool and your first query will respond instantly.
For most use cases the lazy connect behavior of PrismaClient
will do. The first query of PrismaClient
creates the connection pool.
import { Module } from '@nestjs/common';
import { PrismaModule } from 'nestjs-prisma';
@Module({
imports: [
PrismaModule.forRoot({
prismaServiceOptions: {
explicitConnect: true,
},
}),
],
})
export class AppModule {}
Pass PrismaClientOptions
options directly to the PrismaClient
.
Apply Prisma middlewares to perform actions before or after db queries.
Additionally, PrismaModule
provides a forRootAsync
to pass options asynchronously.
One option is to use a factory function:
import { Module } from '@nestjs/common';
import { PrismaModule } from 'nestjs-prisma';
@Module({
imports: [
PrismaModule.forRootAsync({
isGlobal: true,
useFactory: () => ({
prismaOptions: {
log: ['info', 'query'],
},
explicitConnect: false,
}),
}),
],
})
export class AppModule {}
You can inject dependencies such as ConfigModule
to load options from .env files.
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PrismaModule } from 'nestjs-prisma';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
PrismaModule.forRootAsync({
isGlobal: true,
useFactory: async (configService: ConfigService) => {
return {
prismaOptions: {
log: [configService.get('log')],
datasources: {
db: {
url: configService.get('DATABASE_URL'),
},
},
},
explicitConnect: configService.get('explicit'),
};
},
inject: [ConfigService],
}),
],
})
export class AppModule {}
Alternatively, you can use a class instead of a factory:
import { Module } from '@nestjs/common';
import { PrismaModule } from 'nestjs-prisma';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
PrismaModule.forRootAsync({
isGlobal: true,
useClass: PrismaConfigService,
}),
],
})
export class AppModule {}
Create the PrismaConfigService
and extend it with the PrismaOptionsFactory
import { Injectable } from '@nestjs/common';
import { PrismaOptionsFactory, PrismaServiceOptions } from 'nestjs-prisma';
@Injectable()
export class PrismaConfigService implements PrismaOptionsFactory {
constructor() {
// TODO inject any other service here like the `ConfigService`
}
createPrismaOptions(): PrismaServiceOptions | Promise<PrismaServiceOptions> {
return {
prismaOptions: {
log: ['info', 'query'],
},
explicitConnect: true,
};
}
}
Apply Prisma Middlewares with PrismaModule
import { Module } from '@nestjs/common';
import { PrismaModule } from 'nestjs-prisma';
@Module({
imports: [
PrismaModule.forRoot({
prismaServiceOptions: {
middlewares: [
async (params, next) => {
// Before query: change params
const result = await next(params);
// After query: result
return result;
},
], // see example loggingMiddleware below
},
}),
],
})
export class AppModule {}
Here is an example for using a Logging middleware.
Create your Prisma Middleware and export it as a function
// src/logging-middleware.ts
import { Prisma } from '@prisma/client';
export function loggingMiddleware(): Prisma.Middleware {
return async (params, next) => {
const before = Date.now();
const result = await next(params);
const after = Date.now();
console.log(
`Query ${params.model}.${params.action} took ${after - before}ms`
);
return result;
};
}
Now import your middleware and add the function into the middlewares
array.
import { Module } from '@nestjs/common';
import { PrismaModule } from 'nestjs-prisma';
import { loggingMiddleware } from './logging-middleware';
@Module({
imports: [
PrismaModule.forRoot({
prismaServiceOptions: {
middlewares: [loggingMiddleware()],
},
}),
],
})
export class AppModule {}