paint-brush
Create Rest API with Mongoose and Plumier Delightfullyby@ktutnik
1,448 reads
1,448 reads

Create Rest API with Mongoose and Plumier Delightfully

by Ketut SandiarsaSeptember 23rd, 2019
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Plumier has a Mongoose helper to help create mongoose model from your domain model. This feature help you focus only on the logic of your API, and skip the ceremonial setup of validation and database schemas. Plumier also has some features that make developing secure and robust API easy and fun. The more domain model you add the more repetitive code will be required. The Plumier Way is simple and easy to use with the help of Plumier helper for Mongoose and Plumier.

People Mentioned

Mention Thumbnail

Company Mentioned

Mention Thumbnail
featured image - Create Rest API with Mongoose and Plumier Delightfully
Ketut Sandiarsa HackerNoon profile picture

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.

import { Document } from "mongoose"

interface User extends Document {
    email: string,
    name: string,
    birthDate: Date
}

Then you need to create Mongoose schema and repeat declaration of domain model properties for the schema like below.

import mongoose, { Schema } from "mongoose"

const UserModel = mongoose.model<User>("User", new Schema({
    email: String,
    name: String,
    birthDate: Date
}))

Optionally if you thinking about application security and robustness you need to repeat another properties declaration for schema validation for Joi like below.

import joi from "joi"

const userValidator = joi.object({
    email: joi.string().email(),
    name: joi.string(),
    birthDate: joi.date()
})

Above steps not only repetitive and boring but possibly can cause some issue in the future:

  1. Renaming field in the domain model will need to modify the appropriate field on the validation schema and Mongoose schema.
  2. Adding new fields or removing fields also require modification on all those schemas.Increase amount of code written to the project.
  3. The more domain model you add the more repetitive code will be required.

# The Plumier Way

Plumier has a Mongoose helper

@plumier/mongoose
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 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

@plumier/mongoose package
. Then use the
MongooseFacility
from the Plumier application starter.

import Plumier from "plumier"
import { MongooseFacility } from "@plumier/mongoose"

const plum = new Plumier() 
plum.set(new MongooseFacility({
    uri: "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

@collection()
decorator.

import { val } from "plumier"
import { collection } from "@plumier/mongoose"

@collection()
class User {
    constructor(
        @val.email()
        public email: string,
        public name: string,
        public birthDate: 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.

import { model } from "@plumier/mongoose"

const UserModel = model(User);

This simple function call will take care of Mongoose schema generation based on

User
domain model.
model
function is a generic function it will infer provided domain model data type and make generated Mongoose model have proper Mongoose document properties. 

Implementation

Next step you can use above domain model and Mongoose model in your API implementation inside controller like below.

import { route } from "plumier"

class UsersController {
    @route.post("")
    async save(data: User) {
        const result = await new UserModel(data).save()
        return { newId: result.id }
    }
}

There are four free process happened on above controller.

  1. Route will automatically generated based on controller metadata. Above code the save method will handle POST /users request. Refer to Plumier documentation about routing for more information.
  2. 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 documentation for more information.
  3. Data conversion happened in all method parameters based on their data type. Plumier tolerate some string value convertible into data type such as
    ON OFF 1 0 YES NO TRUE FALSE
    convertible to boolean data type etc. Refer to Plumier documentation about conversion.
  4. 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

User
domain model above has
role
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. 

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 Parameter Authorization to secure specific property of the domain model only accessible to specific role like below.

import { val } from "plumier"
import { collection } from "@plumier/mongoose"
import { authorize } from "@plumier/jwt"

@collection()
class User {
    constructor(
        @val.email()
        public email: string,
        public name: string,
        public birthDate: Date,
        @authorize.role("Admin")
        public role:string
    ) { }
}

Above code showing that role property decorated with

@authorize.role("Admin")
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
role
property set) Plumier will automatically response http status 401 Unauthorized with an informative error message.

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 populate both for object nesting and array nesting.

import { val } from "plumier"
import { collection } from "@plumier/mongoose"

@collection()
class Address {
    constructor(
        public address:string,
        public city:string,
        public state: string,
        public zip:string
    ){}
}

@collection()
class User {
    constructor(
        @val.email()
        public email: string,
        public name: string,
        public birthDate: Date,
        public address:Address
    ) { }
}

When using

User
domain model above,
address
property will be registered as populate property. Mongoose schema for the
User
domain model will be generated on the background like below.

Schema({
    email: String,
    name: String,
    birthDate: Date
    address: [{ type: Schema.Types.ObjectId, ref: '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.

import { val } from "plumier"
import { collection } from "@plumier/mongoose"
import reflect from "tinspector"

@collection()
class Image {
    constructor(
        public name: string,
        public uploadDate: string
    ) { }
}

@collection()
class User {
    constructor(
        @val.email()
        public email: string,
        public name: string,
        public birthDate: Date,
        @reflect.array(Image)
        public images:Image[]
    ) { }
}

By using above code, the

images
property will be treated as Mongoose populate property. Important thing about above code is the
@reflect.array(Image)
to specify that the item type of the
images
property is of type
Image
domain model. Note that this trick only required on nested array, its not required on nested object like previously showed on the
address
example.

Custom Schema

Mongoose helper provided flexibility for you to configure the generated Mongoose schema. You can intercept schema generation on from the

MongooseFacility
and provided a callback function for all domain models generated.

For example if you want to enable the Mongoose timestamp function for each domain model, you can provided callback function like below.

import Mongoose from "mongoose"

new MongooseFacility({
     uri: "mongodb://localhost:27017/test-data",
     schemaGenerator: (def, meta) => {
         return new Mongoose.Schema(def, {timestamps: true})
     }
 })

Refer to Mongoose helper documentation for more information about parameters on the callback function.

Unique Validation

Mongoose has a unique 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. 

Plumier provided a built in unique validation merged with the Plumier validation decorators. As an example if you want to make

email
property of our previous domain model unique you can decorate domain model like below.

import { val } from "plumier"
import { collection } from "@plumier/mongoose"

@collection()
class User {
    constructor(
        @val.email()
        @val.unique()
        public email: string,
        public name: string,
        public birthDate: Date
    ) { }
}

By adding

@val.unique()
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.

Note that the

@val.unique()
decorator is Mongoose specific, it will not available if the
@plumier/mongoose
package not installed.

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

ObjectId
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
ObjectId
and Mongoose not smart enough to automatically convert them into
ObjectId
.

Mongoose helper has an object converter it will automatically convert all the nested string id into

ObjectId
so no further conversion required inside controller. 

For example to implement controller on our previous domain model below.

import { val } from "plumier"
import { collection } from "@plumier/mongoose"
import reflect from "tinspector"

@collection()
class Image {
    constructor(
        public name: string,
        public uploadDate: string
    ) { }
}

@collection()
class User {
    constructor(
        @val.email()
        public email: string,
        public name: string,
        public birthDate: Date,
        @reflect.array(Image)
        public images:Image[]
    ) { }
}

Simply save the data passed through the controller parameter without further processing like below.

import { route } from "plumier"

class UsersController {
    @route.post("")
    async save(data: User) {
        const result = await new UserModel(data).save()
        return { newId: result.id }
    }
}

The JSON body sent for controller above simply just a plain JSON like below.

{
    "email": "[email protected]",
    "name": "John Doe",
    "birthDate": "1991-1-2",
    "images": [
        "507f191e810c19729de860ea", 
        "507f191e810c19729de239ca"
    ]
}

Plumier will automatically transformed the string ids inside

images
array into array of
ObjectId
. This feature will keep your controller logic slim and clean.

# 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.