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:upNeed to apply migration
token-ttl-indexesto database This migration create TTL indexes forrefreshTokenandaccessTokenfields inTokenWhiteListmodel. 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. PrismaServicewill 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 {}