nestjsx-crudx v2.4.1
NestJs CRUD for RESTful APIs
@nestjsx/crud
has been designed for creating CRUD controllers and services for RESTful applications built with NestJs. It can be used with TypeORM repositories for now, but Mongoose functionality perhaps will be available in the future.
Features and merits
- CRUD endpoints generation, based on a repository service and an entity.
- Ability to generate CRUD endpoints with predefined path filter.
- Composition of controller methods instead of inheritance (no tight coupling and less surprises)
- Overriding controller methods with ease.
- Request validation.
- Query parameters parsing with filters, pagination, sorting, joins, nested joins, etc.
- Super fast DB query building.
- Additional handy decorators.
Table of Contents
- Install
- Getting Started
- API Endpoints
- Swagger
- Query Parameters
- Repository Service
- Crud Controller
- Example Project
- Contribution
- Tests
- License
Install
npm i @nestjsx/crud --save
npm i @nestjs/typeorm typeorm class-validator class-transformer --save
Getting Started
Entity
Assume you have some TypeORM enitity:
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class Hero {
@PrimaryGeneratedColumn() id: number;
@Column() name: string;
}
Service
Next, let's create a Repository Service for it:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { RepositoryService } from '@nestjsx/crud/typeorm';
import { Hero } from './hero.entity';
@Injectable()
export class HeroesService extends RepositoryService<Hero> {
constructor(@InjectRepository(Hero) repo) {
super(repo);
}
}
Just like that!
Controller
Next, let create a Crud Controller that expose some RESTful endpoints for us:
import { Controller } from '@nestjs/common';
import { Crud } from '@nestjsx/crud';
import { Hero } from './hero.entity';
import { HeroesService } from './heroes.service';
@Crud(Hero)
@Controller('heroes')
export class HeroesController {
constructor(public service: HeroesService) {}
}
And that's it, no more inheritance and tight coupling. Let's see what happens here:
@Crud(Hero)
We pass our Hero
entity as a dto
for Validation purpose and inject HeroesService
. After that, all you have to do is to hook up everything in your module. And after being done with these simple steps your application will expose these endpoints:
API Endpoints
Get Many Entities
GET /heroes
GET /heroes/:heroId/perks
Result: array of entities | empty array
Status Codes: 200
Get One Entity
GET /heroes/:id
GET /heroes/:heroId/perks:id
Request Params: :id
- entity id
Result: entity object | error object
Status Codes: 200 | 404
Create One Entity
POST /heroes
POST /heroes/:heroId/perks
Request Body: entity object | entity object with nested (relational) objects
Result: created entity object | error object
Status Codes: 201 | 400
Create Many Entities
POST /heroes/bulk
POST /heroes/:heroId/perks/bulk
Request Body: array of entity objects | array of entity objects with nested (relational) objects
{
"bulk": [{ "name": "Batman" }, { "name": "Batgirl" }]
}
Result: array of created entitie | error object
Status codes: 201 | 400
Update One Entity
PATCH /heroes/:id
PATCH /heroes/:heroId/perks:id
Request Params: :id
- entity id
Request Body: entity object (or partial) | entity object with nested (relational) objects (or partial)
Result:: updated partial entity object | error object
Status codes: 200 | 400 | 404
Delete One Entity
DELETE /heroes/:id
DELETE /heroes/:heroId/perks:id
Request Params: :id
- entity id
Result:: empty | error object
Status codes: 200 | 404
Swagger
Swagger support is present out of the box, including Query Parameters and Path Filter.
Query Parameters
GET
endpoints that are generated by CRUD controller support some useful query parameters (all of them are optional):
fields
- get selected fields in GET resultfilter
(alias:filter[]
) - filter GET result byAND
type of conditionor
(alias:or[]
) - filter GET result byOR
type of conditionsort
(alias:sort[]
) - sort GET result by somefield
inASC | DESC
orderjoin
(alias:join[]
) - receive joined relational entities in GET result (with all or selected fields)limit
(aliasper_page
) - receiveN
amount of entitiesoffset
- offsetN
amount of entitiespage
- receive a portion oflimit
(per_page
) entities (alternative tooffset
)cache
- reset cache (if was enabled) and receive entities from the DB
fields
Selects fields that should be returned in the reponse body.
Syntax:
?fields=field1,field2,...
Example:
?fields=email,name
filter
Adds fields request condition (multiple conditions) to you request.
Syntax:
?filter=field||condition||value
Examples:
?filter=name||eq||batman
?filter=isVillain||eq||false&filter=city||eq||Arkham (multiple filters are treated as a combination of
AND
type of conditions)?filter=shots||in||12,26 (some conditions accept multiple values separated by commas)
?filter=power||isnull (some conditions don't accept value)
Alias: filter[]
filter conditions
(condition - operator
):
eq
(=
, equal)ne
(!=
, not equal)gt
(>
, greater than)lt
(<
, lower that)gte
(>=
, greater than or equal)lte
(<=
, lower than or equal)starts
(LIKE val%
, starts with)ends
(LIKE %val
, ends with)cont
(LIKE %val%
, contains)excl
(NOT LIKE %val%
, not contains)in
(IN
, in range, accepts multiple values)notin
(NOT IN
, not in range, accepts multiple values)isnull
(IS NULL
, is NULL, doesn't accept value)notnull
(IS NOT NULL
, not NULL, doesn't accept value)between
(BETWEEN
, between, accepts two values)
or
Adds OR
conditions to the request.
Syntax:
?or=field||condition||value
It uses the same filter conditions.
Rules and examples:
- If there is only one
or
present (withoutfilter
) then it will be interpreted as simple filter:
?or=name||eq||batman
- If there are multiple
or
present (withoutfilter
) then it will be interpreted as a compination ofOR
conditions, as follows:WHERE {or} OR {or} OR ...
?or=name||eq||batman&or=name||eq||joker
- If there are one
or
and onefilter
then it will be interpreted asOR
condition, as follows:WHERE {filter} OR {or}
?filter=name||eq||batman&or=name||eq||joker
- If present both
or
andfilter
in any amount (one or miltiple each) then both interpreted as a combitation ofAND
conditions and compared with each other byOR
condition, as follows:WHERE ({filter} AND {filter} AND ...) OR ({or} AND {or} AND ...)
?filter=type||eq||hero&filter=status||eq||alive&or=type||eq||villain&or=status||eq||dead
Alias: or[]
sort
Adds sort by field (by multiple fields) and order to query result.
Syntax:
?sort=field,ASC|DESC
Examples:
?sort=name,ASC
?sort=name,ASC&sort=id,DESC
Alias: sort[]
join
Receive joined relational objects in GET result (with all or selected fields). You can join as many relations as allowed in your Restful Options.
Syntax:
?join=relation
?join=relation||field1,field2,...
?join=relation1||field11,field12,...&join=relation1.nested||field21,field22,...&join=...
Examples:
?join=profile
?join=profile||firstName,email
?join=profile||firstName,email&join=notifications||content&join=tasks
?join=relation1&join=relation1.nested&join=relation1.nested.deepnested
Notice: id
field always persists in relational objects. To use nested relations, the parent level MUST be set before the child level like example above.
Alias: join[]
limit
Receive N
amount of entities.
Syntax:
?limit=number
Example:
?limit=10
Alias: per_page
offset
Offset N
amount of entities
Syntax:
?offset=number
Example:
?offset=10
page
Receive a portion of limit
(per_page
) entities (alternative to offset
). Will be applied if limit
is set up.
Syntax:
?page=number
Example:
?page=2
cache
Reset cache (if was enabled) and receive entities from the DB.
Usage:
?cache=0
Repository Service
RepositoryService
is the main class where all DB operations related logic is in place.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { RepositoryService } from '@nestjsx/crud/typeorm';
import { RestfulOptions } from '@nestjsx/crud';
import { Hero } from './hero.entity';
@Injectable()
export class HeroesService extends RepositoryService<Hero> {
protected options: RestfulOptions = {};
constructor(@InjectRepository(Hero) repo) {
super(repo);
}
}
This class can accept optional parameter called options
that will be used as default options for GET
requests. All fields inside that parameter are otional as well.
Restful Options
allow option
An Array of fields that are allowed to receive in GET
request. If empty or undefined - allow all.
{
allow: ['name', 'email'];
}
exclude option
an Array of fields that will be excluded from the GET
response (and not queried from the DB).
{
exclude: ['accessToken'];
}
persist option
An Array of fields that will be always persisted in GET
response
{
persist: ['createdAt'];
}
Notice: id
field always persists automatically.
filter option
An Array of filter
objects that will be merged (combined) with query filter if those are passed in GET
request. If not - filter
will be added to the DB query as a stand-alone condition.
If fultiple items are added, they will be interpreted as AND
type of conditions.
{
filter: [
{
field: 'deleted',
operator: 'ne',
value: true,
},
];
}
operator
property is the same as filter conditions.
join option
An Object of relations that allowed to be fetched by passing join query parameter in GET
requests.
{
join: {
profile: {
persist: ['name']
},
tasks: {
allow: ['content'],
},
notifications: {
exclude: ['token']
},
company: {},
'company.projects': {}
}
}
Each key of join
object must strongly match the name of the corresponding entity relation. If particular relation name is not present in this option, then user will not be able to join it in GET
request.
Each relation option can have allow, exclude and persist. All of them are optional as well.
sort option
An Array of sort
objects that will be merged (combined) with query sort if those are passed in GET
request. If not - sort
will be added to the DB query as a stand-alone condition.
{
sort: [
{
field: 'id',
order: 'DESC',
},
];
}
limit option
Default limit that will be aplied to the DB query.
{
limit: 25,
}
maxLimit option
Max amount of results that can be queried in GET
request.
{
maxLimit: 100,
}
Notice: it's strongly recommended to set up this option. Otherwise DB query will be executed without any LIMIT if no limit was passed in the query or if the limit option hasn't been set up.
cache option
If Caching Results is implemented on you project, then you can set up default cache
in milliseconds for GET
response data.
{
cache: 2000,
}
Cache.id
strategy is based on a query that is built by a service, so if you change one of the query parameters in the next request, the result will be returned by DB and saved in the cache.
Cache can be reseted by using the query parameter in your GET
requests.
Crud Controller
Our newly generated working horse. @Crud()
decorator accepts two arguments - Entity class and CrudOptions
object. Let's dive in some details.
Restful Options merge
...
import { Crud } from '@nestjsx/crud';
@Crud(Hero, {
options: {
// RestfulOptions goes here
}
})
@Controller('heroes')
export class HeroesCrudController {
constructor(public service: HeroesService) {}
}
CrudOptions
object may have options
parameter which is the same object as Restful Options.
Notice: If you have this options set up in your RepositoryService
, in that case they will be merged.
Path Filter
CrudOptions
object may have params
parameter that will be used for auto filtering by URL path parameters.
Assume, you have an entity User
that belongs to some Company
and has a field companyId
. And you whant to create UsersController
so that an admin could access users from his own Company only. Let's do this:
...
import { Crud } from '@nestjsx/crud';
@Crud(Hero, {
params: ['companyId']
})
@Controller('/company/:companyId/users')
export class UsersCrud {
constructor(public service: UsersService) {}
}
In this example you're URL param name companyId
should match the name of User.companyId
field. If not, you can do mapping, like this:
...
import { Crud } from '@nestjsx/crud';
@Crud(Hero, {
params: {
company: 'companyId'
}
})
@Controller('/company/:company/users')
export class UsersCrud {
constructor(public service: UsersService) {}
}
Where company
is the name of the URL param, and companyId
is the name of the entity field.
As you might guess, all request will add companyId
to the DB queries alongside with the :id
of GET
, PATCH
, DELETE
requests. On POST
(both: one and bulk) requests, companyId
will be added to the dto
automatically.
When you done with the controller, you'll need to add some logic to your AuthGuard
or any other interface, where you do the authorization of a requester. You will need to match companyId
URL param with the user.companyId
entity that has been validated from the DB.
Validation
Request data validation is performed by using class-validator package and ValidationPipe. If you don't use this approach in your project, then you can implementat request data validation on your own.
We distinguish request validation on create
and update
methods. This was achieved by using validation groups.
Let's take a look at this example:
import { Entity, Column, JoinColumn, OneToOne } from 'typeorm';
import {
IsOptional,
IsString,
MaxLength,
IsNotEmpty,
IsEmail,
IsBoolean,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
import { CrudValidate } from '@nestjsx/crud';
import { BaseEntity } from '../base-entity';
import { UserProfile } from '../users-profiles/user-profile.entity';
const { CREATE, UPDATE } = CrudValidate;
@Entity('users')
export class User extends BaseEntity {
@IsOptional({ groups: [UPDATE] }) // validate on PATCH only
@IsNotEmpty({ groups: [CREATE] }) // validate on POST only
@IsString({ always: true }) // validate on both
@MaxLength(255, { always: true })
@IsEmail({ require_tld: false }, { always: true })
@Column({ type: 'varchar', length: 255, nullable: false, unique: true })
email: string;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsBoolean({ always: true })
@Column({ type: 'boolean', default: true })
isActive: boolean;
@Column({ nullable: true })
profileId: number;
// validate relations, that could be saved/updated as nested objects
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@ValidateNested({ always: true })
@Type((t) => UserProfile)
@OneToOne((type) => UserProfile, (p) => p.user, { cascade: true })
@JoinColumn()
profile: UserProfile;
}
You can import CrudValidate
enum and set up validation rules for each field on firing of POST
, PATCH
requests or both of them.
You can pass you custom validation options here:
import { Crud } from '@nestjsx/crud';
@Crud(Hero, {
validation: {
validationError: {
target: false,
value: false
}
}
})
@Controller('heroes')
...
IntelliSense
Please, keep in mind that we compose HeroesController.prototype
by the logic inside our @Crud()
class decorator. And there are some unpleasant but not very significant side effects of this approach.
First, there is no IntelliSense on composed methods. That's why we need to use CrudController
interface:
...
import { Crud, CrudController } from '@nestjsx/crud';
@Crud(Hero)
@Controller('heroes')
export class HeroesCrud implements CrudController<HeroesService, Hero> {
constructor(public service: HeroesService) {}
}
This will help to make sure that you're injecting proper Repository Service.
Second, even after adding CrudController
interface you still wouldn't see composed methods, accessible from this
keyword, furthermore, you'll get a TS error. In order to solve this, I've couldn't came up with better idea than this:
...
import { Crud, CrudController } from '@nestjsx/crud';
@Crud(Hero)
@Controller('heroes')
export class HeroesCrud implements CrudController<HeroesService, Hero> {
constructor(public service: HeroesService) {}
get base(): CrudController<HeroesService, Hero> {
return this;
}
}
Method Override
List of composed base methods:
getManyBase(
@Param() params: ObjectLiteral,
@Query() query: RestfulParamsDto,
): Promise<T[]>;
getOneBase(
@Param('id') id: number,
@Param() params: ObjectLiteral,
@Query() query: RestfulParamsDto,
): Promise<T>;
createOneBase(
@Param() params: ObjectLiteral,
@Body() dto: T,
): Promise<T>;
createManyBase(
@Param() params: ObjectLiteral,
@Body() dto: EntitiesBulk<T>,
): Promise<T[]>;
updateOneBase(
@Param('id') id: number,
@Param() params: ObjectLiteral,
@Body() dto: T,
): Promise<T>;
deleteOneBase(
@Param('id') id: number,
@Param() params: ObjectLiteral,
): Promise<void>;
Since all composed methods have Base
ending in their names, overriding those endpoints could be done in two ways:
Attach
@Override()
decorator without any argument to the newly created method wich name doesn't containBase
ending. So if you want to overridegetManyBase
, you need to creategetMany
method.Attach
@Override('getManyBase')
decorator with passed base method name as an argument if you want to override base method with a function that has a custom name.
...
import {
Crud,
CrudController,
Override,
RestfulParamsDto
} from '@nestjsx/crud';
@Crud(Hero)
@Controller('heroes')
export class HeroesCrud implements CrudController<HeroesService, Hero> {
constructor(public service: HeroesService) {}
get base(): CrudController<HeroesService, Hero> {
return this;
}
@Override()
getMany(@Param() params, @Query() query: RestfulParamsDto) {
// do some stuff
return this.base.getManyBase(params, query);
}
@Override('getOneBase')
getOneAndDoStuff() {
// do some stuff
}
}
Additional Decorators
There are two additional decorators that come out of the box: @Feature()
and @Action()
:
...
import { Feature, Crud, CrudController } from '@nestjsx/crud';
@Feature('Heroes')
@Crud(Hero)
@Controller('heroes')
export class HeroesController {
constructor(public service: HeroesService) {}
}
You can use them with your ACL implementation. @Action()
will be applyed automaticaly on controller compoesd base methods. There is CrudActions
enum that you can import and use:
enum CrudActions {
ReadAll = 'Read-All',
ReadOne = 'Read-One',
CreateOne = 'Create-One',
CreateMany = 'Create-Many',
UpdateOne = 'Update-One',
DeleteOne = 'Delete-One',
}
ACLGuard
dummy example:
import { Reflector } from '@nestjs/core';
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { getFeature, getAction } from '@nestjsx/crud';
@Injectable()
export class ACLGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const handler = ctx.getHandler();
const controller = ctx.getClass();
const feature = getFeature(controller);
const action = getAction(handler);
console.log(`${feature}-${action}`); // e.g. 'Heroes-Read-All'
return true;
}
}
Example Project
Here you can find an example project that uses @nestjsx/crud
features. In order to run it and play with it, please do the following:
- If you're using Visual Studio Code it's recommended to add this option to your User Settings:
"javascript.implicitProjectConfig.experimentalDecorators": true
Or you can open integration/typeorm
folder separately in the Visual Studio Code.
- Clone the project
git clone https://github.com/nestjsx/crud.git
cd crud/integration/typeorm
Install Docker and Docker Compose if you haven't done it yet.
Run Docker services:
docker-compose up -d
- Run application:
npm run serve
Server should start on default port 3333
, you can override in PORT
environment variable.
If you want to flush the DB data, run:
npm run db:flush
Contribution
Any support is wellcome. Please open an issue or submit a PR if you want to improve the functionality or help with testing edge cases.
Tests
docker-compose up -d
npm run test:e2e