This is starter of a Nest.js 10 application with Prisma ORM, PostgreSQL and Redis.
- JWT Authentication
- CASL Integration
- Simple query builder
- Data Pagination
- Data Sorting
- Data Filtering
- Exception Filters
- Validation Pipes
- Swagger Documentation
- Docker Compose
- PostgreSQL
- Redis
- Serializers
- Health Check
- Prisma
- Redis
- Twilio
- AWS S3
- AWS SQS
- Nest.js 10
- Docker
- Docker Compose
- Node.js
- NPM
- Create volume for PostgreSQL database
docker volume create --name postgres_data0 -d local
- Start the Docker containers using docker-compose
docker-compose up -d
- Install dependencies
npm install
- Generate Prisma Types
npm run db:generate
- Push PostgreSQL Schema
npm run db:push
- Start the application
npm run start:dev
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 {}