If you ever created a rest api using Express, TypeScript and Mongoose you might notice duplication issue caused by declaration of domain model and Mongoose schema. First you need to create a type safe domain model extends Document interface like below. { Document } User Document { email: , name: , birthDate: } import from "mongoose" interface extends string string Date Then you need to create Mongoose schema and repeat declaration of domain model properties for the schema like below. mongoose, { Schema } UserModel = mongoose.model<User>( , Schema({ email: , name: , birthDate: })) import from "mongoose" const "User" new String String Date Optionally if you thinking about application security and robustness you need to repeat another properties declaration for schema validation for like below. Joi joi userValidator = joi.object({ email: joi.string().email(), name: joi.string(), birthDate: joi.date() }) import from "joi" const Above steps not only repetitive and boring but possibly can cause some issue in the future: Renaming field in the domain model will need to modify the appropriate field on the validation schema and Mongoose schema. Adding new fields or removing fields also require modification on all those schemas.Increase amount of code written to the project. The more domain model you add the more repetitive code will be required. # The Plumier Way Plumier has a Mongoose helper to help create mongoose model from your domain model, it taken care of Mongoose schema generation automatically on the fly based on your domain model metadata reflection. This feature help you focus only on the logic of your API, and skip the ceremonial setup of validation and database schema. @plumier/mongoose Plumier also has some features that make developing secure and robust API easy and fun. Plumier version for above code is like below. Enable The Facility To start using the Mongoose helper you need to install the . Then use the from the Plumier application starter. @plumier/mongoose package MongooseFacility Plumier { MongooseFacility } plum = Plumier() plum.set( MongooseFacility({ uri: })) import from "plumier" import from "@plumier/mongoose" const new new "mongodb://localhost:27017/test-data" Note that you need to provide the MongoDB uri because Mongoose helper will manage the connection, it make sure the connection started before the server started. Define Domain Model First define your domain model type using plain TypeScript class with parameter properties and decorated with decorator. @collection() { val } { collection } () User { ( ) { } } import from "plumier" import from "@plumier/mongoose" @collection class constructor .email() email: , name: , birthDate: @val public string public string public Date Domain model definition above help you define how the schema of the MongoDB document used save to the database. Type safe domain model also help to make editor and compiler happy. Generate Mongoose Model Next step is creating Mongoose model based on your domain model using Plumier helper for Mongoose like below. { model } UserModel = model(User); import from "@plumier/mongoose" const This simple function call will take care of Mongoose schema generation based on domain model. function is a generic function it will infer provided domain model data type and make generated Mongoose model have proper Mongoose document properties. User model Implementation Next step you can use above domain model and Mongoose model in your API implementation inside controller like below. { route } UsersController { .post( ) save(data: User) { result = UserModel(data).save() { newId: result.id } } } import from "plumier" class @route "" async const await new return There are four free process happened on above controller. Route will automatically generated based on controller metadata. Above code the save method will handle POST /users request. Refer to Plumier about routing for more information. documentation Request body automatically bound to data parameter. Plumier will choose parameter of type custom object as a request body for convenience. Plumier has various parameter binding function, refer to Plumier for more information. documentation Data conversion happened in all method parameters based on their data type. Plumier tolerate some string value convertible into data type such as convertible to boolean data type etc. Refer to Plumier about conversion. ON OFF 1 0 YES NO TRUE FALSE documentation Validation also happened before the method executed. Plumier will automatically response with http status 422 Unprocessable Entity with a proper validation message. That’s all the setup you need to use Mongoose and Plumier, most of ceremonial setup automatically handled by Plumier in the background. # Security and Parameter Authorization Using the same class for domain model and and DTO (Data Transfer Object — object used as controller parameters) is a kind of dangerous, because user can set sensitif data on specific domain model properties. For example if the domain model above has properties which will be used to control access to the API, it’s possible for user to set their own role and breach the security easily. User role Other frameworks such as Nest or Loopback (and most of frameworks outside the Node.js world) uses separate DTO and Domain Model best practice to avoid this issue. This trick can solve the issue but introduce duplication issue like previously discussed. An extra process to convert data from DTO to domain model also required which pollute the overall API implementation. Plumier has to secure specific property of the domain model only accessible to specific role like below. Parameter Authorization { val } { collection } { authorize } () User { ( ) { } } import from "plumier" import from "@plumier/mongoose" import from "@plumier/jwt" @collection class constructor .email() email: , name: , birthDate: , .role( ) role: @val public string public string public Date @authorize "Admin" public string Above code showing that role property decorated with which mean that the role property only can be set by Admin. If unauthorized user tries to set the value (provided request body with the property set) Plumier will automatically response http status 401 Unauthorized with an informative error message. @authorize.role("Admin") role Using above feature you can safely use single class for DTO and domain object, without having create them separately. # Minimum Trade Off With all those process and ceremonial setup handled internally by Plumier you might thinking about the cost of those features. Plumier designed with security and performance in mind. All framework parts such as Routing, Validation Engine, Parameter Binder etc, all algorithms and functions used optimized for V8 engine. That’s make Plumier fast and efficient. You can take a look at full stack benchmark result below compared to other TypeScript frameworks. The benchmark project can be found here GET method benchmark starting... Server Base Method Req/s Cost (%) plumier koa GET 33624.00 -0.06 koa GET 33602.19 0.00 express GET 17688.37 0.00 nest express GET 16932.91 4.27 loopback express GET 5174.61 70.75 POST method benchmark starting... Server Base Method Req/s Cost (%) koa POST 12218.37 0.00 plumier koa POST 11196.55 8.36 express POST 9543.46 0.00 nest express POST 6814.64 28.59 loopback express POST 3108.91 67.42 Important thing to note about above result is the Cost (%) score, Cost is percentage loss of request per second score caused by framework internal process such as routing, parameter binding, validation etc. Above result showing that Plumier has better score than Nest and Loopback 4, on the GET method Plumier even has better (nearly the same) score as Koa - Koa Router - Joi stack. # Advanced Stuffs Usually for a simple rest api no further configuration needed to use Mongoose inside Plumier. But there are advanced use case that you need todo some extra configuration to make it work like expected. Nested Document Plumier automatically treated nested document model as both for object nesting and array nesting. populate { val } { collection } () Address { ( ){} } () User { ( ) { } } import from "plumier" import from "@plumier/mongoose" @collection class constructor address: , city: , state: , zip: public string public string public string public string @collection class constructor .email() email: , name: , birthDate: , address:Address @val public string public string public Date public When using domain model above, property will be registered as populate property. Mongoose schema for the domain model will be generated on the background like below. User address User Schema({ email: , name: , birthDate: address: [{ : Schema.Types.ObjectId, ref: }] }) String String Date type 'Address' For nested document within an array, you need to specify extra decorator on the property, since no type information provided by TypeScript after compilation for array type. { val } { collection } reflect () Image { ( ) { } } () User { ( ) { } } import from "plumier" import from "@plumier/mongoose" import from "tinspector" @collection class constructor name: , uploadDate: public string public string @collection class constructor .email() email: , name: , birthDate: , .array(Image) images:Image[] @val public string public string public Date @reflect public By using above code, the property will be treated as Mongoose populate property. Important thing about above code is the to specify that the item type of the property is of type domain model. Note that this trick only required on nested array, its not required on nested object like previously showed on the example. images @reflect.array(Image) images Image address Custom Schema Mongoose helper provided flexibility for you to configure the generated Mongoose schema. You can intercept schema generation on from the and provided a callback function for all domain models generated. MongooseFacility For example if you want to enable the Mongoose timestamp function for each domain model, you can provided callback function like below. Mongoose MongooseFacility({ uri: , schemaGenerator: { Mongoose.Schema(def, {timestamps: }) } }) import from "mongoose" new "mongodb://localhost:27017/test-data" ( ) => def, meta return new true Refer to Mongoose helper for more information about parameters on the callback function. documentation Unique Validation Mongoose has a validation out of the box, but we can’t depends on this validation process because it happened when method of the mongoose object called, so Plumier can’t provide automatic validation error response like previously mentioned. unique Plumier provided a built in unique validation merged with the Plumier validation decorators. As an example if you want to make property of our previous domain model unique you can decorate domain model like below. email { val } { collection } () User { ( ) { } } import from "plumier" import from "@plumier/mongoose" @collection class constructor .email() .unique() email: , name: , birthDate: @val @val public string public string public Date By adding Plumier will automatically check to appropriate collection and make sure the provided email is unique before the controller executed. It will response proper validation error response with a proper message if found invalid data. @val.unique() Note that the decorator is Mongoose specific, it will not available if the package not installed. @val.unique() @plumier/mongoose Submit Nested Document Sometime when working with nested document, the child document created using different resource url then the parent document created by populated the id of the child document. Real world example of this use case is creating user with images like our previous example, where the images should be uploaded separately. The problem is saving nested child document require the of the child document, it will be a problem when you send the child id using JSON because its a type of string instead of and Mongoose not smart enough to automatically convert them into . ObjectId ObjectId ObjectId Mongoose helper has an object converter it will automatically convert all the nested string id into so no further conversion required inside controller. ObjectId For example to implement controller on our previous domain model below. { val } { collection } reflect () Image { ( ) { } } () User { ( ) { } } import from "plumier" import from "@plumier/mongoose" import from "tinspector" @collection class constructor name: , uploadDate: public string public string @collection class constructor .email() email: , name: , birthDate: , .array(Image) images:Image[] @val public string public string public Date @reflect public Simply save the data passed through the controller parameter without further processing like below. { route } UsersController { .post( ) save(data: User) { result = UserModel(data).save() { newId: result.id } } } import from "plumier" class @route "" async const await new return The JSON body sent for controller above simply just a plain JSON like below. { : , : , : , : [ , ] } "email" "john.doe@gmail.com" "name" "John Doe" "birthDate" "1991-1-2" "images" "507f191e810c19729de860ea" "507f191e810c19729de239ca" Plumier will automatically transformed the string ids inside array into array of . This feature will keep your controller logic slim and clean. images ObjectId # Final Words Plumier is a new framework, it has many features that will make your development time easy and delightful. If you like it please support the project by star it on . GitHub