Nest.js + Mongoose
A simple solution to create a CRUD controller for a MongoDB collection.
Features
- CRUD operation
- Create a document
- Read: One or a list of document
- Update a document
- Delete a document
- Complete OpenApi documentation
- Convert a DTO (public object) to/from Mongo Entity (internal object)
- Fine control on what is available (CRUD, fields, sorting)
- Multiple input and output format (built-in: Json:api, HAL, JSON+LD and JSON)
- Input (Create and Update): Json:api and JSON
- Output (Read): Json:api, HAL, JSON+LD and JSON
- Standard error output format (RFC-9457)
- Full-customizable
- Override controller routes
- Add your custom input and output format
- Custom DTO converter
- And much more...
(Json:api and HAL format are enabled by default)
Installation
NPM
npm install @macfja/nestjs-mongoose
Usage
Nest Module
In your main module (i.e. src/app.module.ts
) configure your MongooseModule.
Something similar to:
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { CatController, DogController } from "./app.controller";
import { CatController, DogController } from "./cat/cat.controller";
import { Cat, CatSchema } from "./cat/cat.schema";
import { AppService } from "./app.service";
@Module({
imports: [
MongooseModule.forRoot("mongodb://root:root@localhost:27017/example?authSource=admin"),
MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }]),
],
controllers: [AppController, CatController],
providers: [AppService],
})
export class AppModule {}
Mongoose Schema
Declare your mongoose schema as usual.
Controller
In your controller (i.e. src/cat/cat.controller.ts
)
import { MongooseControllerFactory } from "@macfja/nestjs-mongoose";
import { Controller } from "@nestjs/common";
import { ApiTags } from "@nestjs/swagger";
import { CatConverter } from "./cat.converter";
import { CatDto } from "./cat.dto";
@Controller("cats")
@ApiTags("Cat Api")
export class CatController extends MongooseControllerFactory(Cat.name, new CatConverter(), CatDto, CatDto, CatDto) {}
To get the automatic CRUD controller, we need to extend the function MongooseControllerFactory
, which take 6 parameters (Only the first two are mandatory):
declare function MongooseControllerFactory<Resource, Dto extends JsonObject, Creator extends JsonObject, Updater extends JsonObject>(
modelInjectionName: string,
converter: EntityConverter<Resource, Dto, Creator, Updater>,
dtoConstructor?: Type<Dto>,
creatorConstructor?: Type<Creator>,
updaterConstructor?: Type<Updater>,
options?: MongooseControllerOptions<Dto, Creator, Updater>
): Type<MongooseController<Dto, Creator, Updater>>;
modelInjectionName
is the name linked to the schema (same as declared in MongooseModule.forFeature
)converter
is the instance responsible to convert your DTO into Mongo Entity and vice-versadtoConstructor
is the class representation of one element of your collection that you want to return
(if missing or undefined
, it's the same as options.disable.read: true
)creatorConstructor
is the class representation of a new element to add to your collection that you want to receive
(if missing or undefined
, it's the same as options.disable.create: true
)updaterConstructor
is the class representation of one element to update in your collection that you want to receive
(if missing or undefined
, it's the same as options.disable.update: true
)options
is a set of configuration to change what the controller can do (more information later in this document).
DTO
Let's see how the DTO (i.e. src/cat/cat.dto.ts
) are:
import { BaseDto } from "@macfja/nestjs-mongoose";
import { ApiProperty } from "@nestjs/swagger";
export class CatDto extends BaseDto<CatDtoType> {
constructor(name: string, breed: string, age: number) {
super();
this.name = name;
this.breed = breed;
this.age = age;
}
@ApiProperty()
name: string;
@ApiProperty()
breed: string;
@ApiProperty()
age: number;
}
export type CatDtoType = {
name: string;
breed: string;
age: number;
};
BaseDto
is a helper class to ease the typing, and it's completely optional.
Converter
Let's take a look on the converter (i.e. src/cat/cat.converter.ts
):
import {
type DotKeys,
type EntityConverter,
type PartialWithId,
type SearchField,
toMongoFilterQuery,
toMongoSort,
} from "@macfja/nestjs-mongoose";
import { type FilterQuery, type HydratedDocument, type SortOrder, Types } from "mongoose";
import { CatDto } from "./cat.dto";
import type { Cat } from "./cat.schema";
export class CatConverter implements EntityConverter<Cat, CatDto, CatDto, CatDto> {
fromDtoFields(fields?: Array<DotKeys<CatDto>>): Array<DotKeys<Cat>> {
return fields
?.filter((field) => ["name", "age", "breed"].includes(field))
.map((field) => {
switch (field) {
case "name":
return "name";
case "age":
return "age";
case "breed":
return "breed";
}
return false;
})
.filter(Boolean) as Array<DotKeys<Cat>>;
}
fromDtoSort(sort?: Array<string>): Record<string, SortOrder> {
return toMongoSort(sort ?? []);
}
toDto(input: HydratedDocument<Cat>): Partial<CatDto> {
return new CatDto(input.name, input.breed, input.age);
}
fromSearchable(input?: SearchField<CatDto>): FilterQuery<Cat> {
return toMongoFilterQuery(input);
}
fromCreator(input: Partial<CatDto>): Partial<Cat> {
return {
...input,
};
}
fromUpdater(id: string, input: Partial<CatDto>): PartialWithId<Cat> {
return {
...input,
_id: new Types.ObjectId(id),
};
}
}
The converter do 6 transformation:
fromDtoFields()
allow you to define the list of field to select (projection
) in the MongoDB query from the name of field of the DTOfromDtoSort()
allow to set how the document are sorted. The parameter is a list of field name of the DTO (can be prefixed by -
to reverse the order).fromSearchable()
allow to change the filtering provided (The operators are not exactly the same as MongoDB, set later in this document)fromCreator()
allow you to transform a creation DTO into a Mongoose entity datafromUpdater()
allow you to transform a modification DTO into a Mongoose entity datatoDto()
allow you to transform a document from MongoDB into the DTO you want to display
@macfja/nestjs-mongoose
come with all sort of helper to ease the creation of a converter:
toMongoFilterQuery()
: Transform a received filter into a MongoDB query filtertoMongoSort()
: Transform a list of field name and negate field name into a MongoDB sort parameterclass OneToOneConverter
: A preconfigured converter that output a MongoDB Entity as is come from the database
With this you should have a functional CRUD for your MongoDB collection.
Configuration
As mentioned earlier, MongooseControllerFactory
has a 6th parameter to control how it's works:
type MongooseControllerOptions<Dto extends JsonObject, Creator extends JsonObject, Updater extends JsonObject> = Partial<{
disable: Partial<{
list: boolean;
get: boolean;
update: boolean;
create: boolean;
delete: boolean;
read: boolean;
write: boolean;
}>;
pageSize: Partial<{
default: number;
max: number;
}>;
urlResolver: ProblemDetailTypeUrlResolver;
logger: LoggerService;
resourceType: string;
representations: Array<Representation<Dto, Creator, Updater>>;
filter: Partial<{
operators: typeof Operators;
actionOnInvalid: FilterParserAction;
fields: Partial<{
exclude: Array<string>;
add: Array<string>;
}>;
}>;
sort: Partial<{
exclude: Array<string>;
add: Array<string>;
}>;
projection: Partial<{
exclude: Array<string>;
add: Array<string>;
}>;
}>;
disable
, It allow to remove some part of the controller:
list
: if true
, the listing of document is removed from the controller (default: false
)get
: if true
, getting one document from its id is removed from the controller (default: false
)update
: if true
, updating one document is removed from the controller (default: false
)create
: if true
, creating a document is removed from the controller (default: false
)delete
: if true
, deleting one document from its id is removed from the controller (default: false
)read
: if true
, the listing and getting one document are removed from the controller, the output of the creation and modification of a document is disabled (default: false
)writing
: if true
, updating, creating and deleting a document are removed from the controller (default: false
)
pageSize
, Allow to change the pagination size:
default
, the default page size if none is provided (default: 10
)max
, the maximum page size allowed. If the value requested by the user is superior, it is set to this value (default: 200
)
urlResolver
, The ProblemDetail
error resolver (default: undefined
=> Resolve URL to https://httpstatuses.com/
)logger
, The logger to use. Used by ProblemDetail
(default: undefined
=> no log)resourceType
, The name of the resource to display in the outputs. Used for Json:Api, HAL (default: undefined
=> same as the modelInjectionName
provided to the factory)representations
, The list of document representation standard to use (default: [instance of JsonApi, instance of Hal]
)filter
, Configuration of the list filter:
operators
, List of operators to display in the swagger (default: [ "$eq", "$neq", "$gt", "$gte", "$lt", "$lte", "$start", "$end", "$regex", "$null", "$def", "$in", "$nin", "$or", "$and" ]
)actionOnInvalid
, Define how the filter parser should handle invalid operator or field. (default: FilterParserAction.THROW
)fields
, Filter fields options:
exclude
, List of field to remove (default: []
)add
, List of field to add (default: []
)
sort
, Sort fields options:
exclude
, List of field to remove (default: []
)add
, List of field to add (default: []
)
projection
, Projection (display) fields options:
exclude
, List of field to remove (default: []
)add
, List of field to add (default: []
)
Custom data representation (input and output)
The library come with 4 built-in representation:
JsonApi
, which implement the {json:api}
spec and support:
- Get a list of document (Response)
- Get one document (Response)
- Update one document (Request)
- Create one document (Request)
HAL
, which implement the HAL spec and support:
- Get a list of document (Response)
- Get one document (Response)
JsonLdFactory(context: string)
, which implement the JSON-LD spec with Hydra spec for collection and support:
- Get a list of document (Response)
- Get one document (Response)
SimpleJson
, which have a minimal encapsulation and support:
- Get a list of document (Response)
- Get one document (Response)
- Update one document (Request)
- Create one document (Request)
To create a new output format, you need an instance of Representation
type Representation<Dto extends JsonObject = any, Creator extends JsonObject = any, Updater extends JsonObject = any> = {
readonly renderOne?: RenderOne<Dto>;
readonly renderPage?: RenderPage<Dto>;
readonly getOneResponseSwaggerExtension?: OneResponseSwaggerExtension<Dto>;
readonly getCollectionResponseSwaggerExtension?: CollectionResponseSwaggerExtension<Dto>;
readonly getCreateRequestSwaggerExtension?: CreateRequestSwaggerExtension<Creator>;
readonly getUpdateRequestSwaggerExtension?: UpdateRequestSwaggerExtension<Updater>;
readonly parseCreateRequest?: ParseCreate<Creator>;
readonly parseUpdateRequest?: ParseUpdate<Updater>;
readonly contentType: string;
};
The property contentType
indicate the output MIME type of your representation, it is also use (with the Accept
header, or Content-type
header) to determine which representation to use.
RenderOne<Resource extends JsonObject> = (id: string, type: string, self: string, resource: Resource) => JsonObject
, this function is call to render a document
id
is the MongoDB id of the documenttype
is the value of resourceType
(or the value of modelInjectionName
)self
is the URL pathname of the controllerresource
is the DTO version of the MongoDB document
RenderPage<Resource extends JsonObject> = (type: string, self: string, count: number, pageData: { size: number; current: number; }, resources: Map<string, Resource>) => JsonObject
, this function is call to render a paginated list of documents
type
is the value of resourceType
(or the value of modelInjectionName
)self
is the URL pathname of the controllerpageData
is the current information about the page (the size of a page, and the current page number)resources
is list of item of the page. The key of the map is the MongoDB id of the document, the value the DTO version
ParseCreate<Creator extends JsonObject> = (input: JsonObject, type: string) => Creator | never
, this function extract the Creator
resource from the representation
input
is the JSON receive by the controllertype
is the value of resourceType
(or the value of modelInjectionName
)- If an error occurs wil validating/parsing the input, a
ProblemDetailException
(or any exception) can be thrown
ParseUpdate<Updater extends JsonObject> = (input: JsonObject, type: string, id: string) => Updater | never
, this function extract the Updater
resource from the representation
input
is the JSON receive by the controllertype
is the value of resourceType
(or the value of modelInjectionName
)id
is the identifier of the document to update- If an error occurs wil validating/parsing the input, a
ProblemDetailException
(or any exception) can be thrown
OneResponseSwaggerExtension
, CollectionResponseSwaggerExtension
, CreateRequestSwaggerExtension
, UpdateRequestSwaggerExtension
all extends the interface, and are used to describe a OpenApi resource
type SwaggerExtensionMaker<Input extends JsonObject = JsonObject> = (
attribute: Type<Input>,
resourceType: string,
) => SwaggerSchemaExtension
attribute
is the class of a DTOresourceType
is the value of resourceType
(of the configuration), or the value of modelInjectionName
The functions return a SwaggerSchemaExtension
to helper generate an accurate OpenApi document
interface SwaggerSchemaExtension {
extraModels: Parameters<typeof ApiExtraModels>;
schema: SchemaObject;
}
extraModels
List of class to inject in the OpenApi definitionschema
The linked OpenApi schema
Override a route
You can change the behavior of a simple route by overriding it like a normal OOP class.
import { GetOneDecorator, Hal, JsonApi, type JsonObject, MongooseControllerFactory } from "@macfja/nestjs-mongoose";
import { Controller } from "@nestjs/common";
import { ApiTags } from "@nestjs/swagger";
import type e from "express";
import { CatConverter } from "./cat.converter";
import { CatDto } from "./cat.dto";
import { Cat } from "./cat.schema";
@Controller("cats")
@ApiTags("Cat Api")
export class CatController extends MongooseControllerFactory(Cat.name, new CatConverter(), CatDto, CatDto, CatDto) {
@GetOneDecorator(CatDto, Cat.name, [JsonApi, Hal])
override async getOne(response: e.Response, request: e.Request, id: string, fields?: unknown, accept?: unknown): Promise<JsonObject> {
const result = await super.getOne(response, request, id, fields, accept);
return result;
}
}
The decorator @GetOneDecorator
add all the annotation needed for the OpenApi documentation and the route declaration.
- use
@CreateOneDecorator
, for the creation route (controller method createOne
) - use
@DeleteOneDecorator
, for the removing route (controller method deleteOne
) - use
@UpdateOneDecorator
, for the modification route (controller method updateOne
) - use
@GetListDecorator
, for the listing route (controller method getList
) - use
@GetOneDecorator
, for the reading route (controller method getOne
)
Filter operator
This library is a slightly different list of operator
@macfja/nestjs-mongoose | MongoDB |
---|
$eq | $eq |
$neq | $ne |
$gt | $gt |
$gte | $gte |
$lt | $lt |
$lte | $lte |
$start | $regex with a altered value |
$end | $regex with a altered value |
$regex | $regex |
$null | $eq with the value to null |
$def | $ne with the value to null |
$in | $in |
$nin | $nin |
$or | $or |
$and | $and |
The library handle the case where several $regex
, $eq
, $ne
would appear in the MongoDB request if the function toMongoFilterQuery()
is used
OpenApi version
The OpenApi documentation generated by the library is in version 3.1.0
.
To enable it in NestJs you need to manually set the version.
In your main.ts
file, you need to change parameter of SwaggerModule.setup
function.
const document = SwaggerModule.createDocument();
SwaggerModule.setup("api", app, { ...document, openapi: "3.1.0" });