Use decorators and javascript to build web services




English | 简体中文

# Introduction

totea is a nodejs framework based on express, using decorators to define routing and middleware. Features overview:

  • javascript: Existing frameworks that use decorators all use typescript by default, totea can be used in javascript, only need to introduce plugin-proposal-decorators (opens new window);
  • Simple and efficient: totea provides less than 20 decorator functions, but it can support a variety of complex usage scenarios;
  • Easy to integrate: Totea uses express as a web server. We have not modified any underlying logic, which means that the methods and plugins that can be used in express can also be used in totea.

# Example

# Minimal example

const { Server } = require('@totea/core')

@Server()
class Service {}

const service = new Service()

service.start() // the app will serve at localhost:3000

# Define routing

const { Server, Get, Post, Delete, Put, Patch } = require('@totea/core')

@Server()
class Service {
  @Get('/root')
  getRoot() {
    return 1
  }

  @Post('/root')
  postRoot() {
    return 1
  }

  @Delete('/root')
  deleteRoot() {
    return 1
  }

  @Patch('/root')
  patchRoot() {
    return 1
  }

  @Put('/root')
  putRoot() {
    return 1
  }
}

const service = new Service()

service.start()

# Use middleware

const { Server, Get, Middleware } = require('@totea/core')

@Server()
@Middleware((req, res, next) => {
  // global middleware
  console.log('call global middleware')
  next()
})
class Service {
  @Get('/root')
  @Middleware((req, res, next) => {
    // Private middleware
    console.log('call specific middleware')
    next()
  })
  getRoot() {
    return 1
  }
}

const service = new Service()

service.start()

# Parameter verification

const { Server, Get, Query } = require('@totea/core')

@Server()
class Service {
  @Get('/root')
  @Query(query => query.id && quey.id.length === 10) // The id field must be included in req.query, the length is 10, otherwise it returns 400
  getRoot() {
    return 1
  }
}

const service = new Service()

service.start()

// >>>>> curl "localhost:3000/root" -X GET
// <<<<< {"status":400,"message":"Bad Request"}

// >>>>> curl "localhost:3000/root?id=1" -X GET
// <<<<< {"status":400,"message":"Bad Request"}

// >>>>> curl "localhost:3000/root?id=1234567890" -X GET
// <<<<< {"status":200,"message":"OK", "result": 1}

# Sub-route

const { Server, Get, Query, Controller } = require('@totea/core')

// use controller define a sub-route
@Controller('childRoute')
class ChildController {
  @Get('/address') // GET /child-route/address
  getAddress() {
    return 'ok'
  }
}

// then provide it to server
@Server({
  controller: [ChildController]
})
class Service {}

const service = new Service()

service.start()

// >>>>> curl "localhost:3000/child-route/address" -X GET
// <<<<< {"status":200,"message":"OK", "result":'ok'}

# Installation and use

# install totea
npm i @totea/core

# Install and configure babel
npm i @babel/core @babel/node @babel/plugin-proposal-decorators

# If there is already a babel configuration, add a decorator plugin
# If not, create a babel configuration file in the project root directory and write the following:
module.exports = {
  plugins: [
    ["@babel/plugin-proposal-decorators", {legacy: true }]
  ],
};

# Use babel-node to run and debug your code
npx babel-node index.js

# API

# Server

const { Server } = require('@totea/core')

# Parameters

The Server decorator is used to define a web server and contains the following parameters:

  • port: The port on which the service runs, integer, default 3000, acceptable port range: 1024-65535;
  • middleware: global middleware, array<function>, the type is an array containing one or more functions;
  • errorMiddleware: global error handling middleware, array<function>, the type is an array containing one or more functions;
  • controller: The secondary routing list, array<Controller|controller>, which can provide Controller or an array of controllers that have been instantiated;
  • onServe: The hook function for the service to start running, function;
  • onClose: The hook function when the service ends, function;
  • onResponse: The callback function before the request response, which can be used to customize the response format, function;
  • slience: Whether to prohibit printing log, boolean, default false;
  • static: Define static file directory, string|{path: string, maxAge: integer}, can accept string type, which means directory, or an obejct, the parameter will be automatically passed to express.static()
  • view: Define the view rendering template, -path: template folder, string, eg:'./views' -engine: template engine, object, the provided value needs to include the __express attribute, eg: require('pug') -type: template engine name, string, eg:'pug'

# example

const { Server, Get, Query, Controller } = require('@totea/core')

@Controller('childRouteA')
class ChildControllerA {}

@Controller('childRouteB')
class ChildControllerB {}

@Server({
  port: 4000,
  onServe() {
    console.log('service is start success')
  },
  onClose() {
    console.log('service is closed')
  },
  errorMiddleware: [
    (error, req, res, next) => {
      console.error('got a error', error)
      next(error)
    }
  ],
  middleware: [
    (req, res, next) => {
      console.log('call global middleware')
      next()
    }
  ],
  controller: [ChildControllerA, ChildControllerB],
  static: './public',
  view: {
    path: './views',
    engine: require('pug'),
    type: 'pug'
  },
  slience: true
})
class Service {}

// You can also pass parameters when instantiating, the parameters here will be merged to cover the part that has been passed in the decorator
const service = new Service({
  slience: false
})

service.start()

# ToteaServer

ToteaServer The class obtained after the Server decorator wraps, can also be directly quoted from totea:

const { ToteaServer } = require('@totea/core')

const service = new ToteaServer()

service.start()

// The above example is equivalent to
const { Server } = require('@totea/core')

@Server()
class ToteaServer {}

const service = new ToteaServer()

service.start()

# Instance attributes

  • app: the express instance created;
  • server: the created http server;
  • runing: Get the current service running status, true means it is running;

# Example method

  • start(): Start running the server;
  • stop(): Stop running the server;
  • status(): Get the current service running status, true means it is running;
  • useController(Controller|controller): Inject a secondary route, which can provide a Controller or an instantiated controller;
  • use(): equivalent to app.use in express;
  • all(): equivalent to app.all in express;
  • get(): equivalent to app.get in express;
  • post(): equivalent to app.post in express;
  • patch(): equivalent to app.patch in express;
  • delete(): equivalent to app.delete in express;
  • put(): equivalent to app.put in express;

# Controller

const { Controller } = require('@totea/core')

# Parameters

The Controller decorator is used to define secondary routes and contains the following parameters:

  • name: controller name, string, cannot contain / characters, we will use humps (opens new window) to format and convert the name by default In camel case format, the bound routing path will be converted to lowercase characters and connected with-

    Input Name(camelize) Path(decamelize, separator: -)
    @Controller(``hello-world_route') helloWorldRoute /hello-world-route
    @Controller(''Phone') phone /phone
    @Controller('/SubRoute') subRoute /sub-route

# example

The following example demonstrates how to obtain a controller instance in the server or each controller through this.controllers.${controllerName}. In addition, in each controller, the server instance can be obtained through this.server. This greatly facilitates code reuse:

const { Server, Controller, Get } = require('../../index')

@Controller('childOne')
class ChildOneController {
  @Get('/address') // GET /child-one/address
  getAlias() {
    return 'childOne'
  }
}

@Controller('child-two')
class ChildTwoController {
  @Get('/address') // GET /child-two/address
  getAlias() {
    // run other controller's method
    const childOneRes = this.controllers.childOne.getAlias()

    // run root server's method
    const rootRes = this.server.getRootAlias()

    return rootRes + '/' + childOneRes
  }
}

@Server({
  controller: [ChildOneController, ChildTwoController]
})
class Service {
  @Get() // GET /root
  root() {
    return 'get/' + this.controllers.childTwo.getAlias()
  }

  getRootAlias() {
    return 'root'
  }
}

const service = new Service()

service.start()

// >>>>> curl "localhost:3000/root"
// <<<<< {"status":200,"result":"get/root/childOne","message":"OK"}

# ToteaController

ToteaController The class obtained after the Controller decorator wraps, can also be directly referenced from totea:

How to write without decorators

const { ToteaServer, ToteaController } = require('@totea/core')

const controller = new ToteaController('child')

const service = new ToteaServer({
  controller: [controller]
})

service.start()

The above example is equivalent to

const { Server, Controller } = require('@totea/core')

@Controller('child')
class ToteaController {}

@Server()
class ToteaServer {}

const service = new ToteaServer({
  controller: [ToteaController]
})

service.start()

# Instance attributes

  • router: Router instance created by the controller;
  • url: The url address bound to the controller;
  • name: the name of the controller;

# Example method

  • getRouter(): Return the Router instance created by the controller;
  • use(): equivalent to router.use of the controller;
  • all(): equivalent to router.all of the controller;
  • get(): equivalent to router.get of the controller;
  • post(): equivalent to router.post of the controller;
  • patch(): equivalent to the router.patch of the controller;
  • delete(): equivalent to router.delete of the controller;
  • put(): equivalent to router.put of the controller;

It can be seen that the Server decorator is actually a Controler decorator. They have basically the same methods. The difference is that in the Server, calling methods such as use | all | get will bind the route to the app, but in the controller , Is bound to the internal router. In express, the app is a special router, isn't it?

Reading the following documents, you can find that the concept in totea is very simple, it provides a basically consistent API and as few decorator methods as possible, which is very easy for developers to master.

# Methods

Methods decorator is used to add routing binding to Server or Controller, including the following:

  • Get: bind get request
  • Post: bind postt request
  • Delete: bind delete request
  • Patch: bind patch request
  • Put: bind put request

Parameters: Indicates the bound routing address, which is the same as the address in express, string|regexp, supports string or regular expression, optional, if the parameter is omitted, the bound function name will be used as the address.

# Examples of errors

Note: The same url and method cannot be bound to different functions, and different urls cannot be bound to the same function. They must correspond one-to-one. Totea can check most error scenarios, but there are some Developers need to be standardized from coding.

@Server()
class Service {
  @Get('/user')
  getUser() {
    return {id: 1, name:'leo'}
  }

  @Get('/user') // ERROR, can not bind /user again
  getUser2() {
    return {id: 2, name:'tony'}
  }
}

// will get a Error
Error: the url: /user has already bound
@Server()
class Service {
  @Get('/user')
  getUser() {
    return {id: 1, name:'leo'}
  }

  @Get('/user2')
  getUser() {// ERROR, can not bind /user2 to getUser
    return {id: 2, name:'tony'}
  }
}
// will get a Error
Error: the callback:getUser has already bound to url: /user, method: get

# Situations to avoid

The following example will not report an error. It seems that it is not wrong to define two same methods in the same class, but we strongly do not recommend this, which will make the code logic very confusing.

@Server()
class Service {
  @Get('/user')
  getUser() {
    return { id: 1, name: 'leo' }
  }

  getUser() {
    // RIGHT, but not recommand
    return { id: 2, name: 'tony' }
  }
}

# RESTful

The same URL can be bound to different functions, provided that different methods are used, such as @Get('/user') @Post('/user') can be bound to different methods respectively, which is often used to create RESTful style Api.

The following example demonstrates the use of totea to create a restful style api interface:

const { Server, Get, Post, Delete, Put } = require('@totea/core')

// mock db
const users = [] // eg {id: 1, name:'leo'}
// mock user.id self increasing
let lastCreateUserId = 0

@Server()
class Service {
  @Get('/user')
  getUser() {
    return users
  }

  @Get('/user/:id')
  getUserById({ params }) {
    if (!params.id) {
      return Promise.reject('please input user id')
    }
    const user = users.filter(item => item.id === parseInt(params.id))[0]

    if (!user) {
      return Promise.reject('this user is unexsist, please recheck')
    }

    return user
  }

  @Post('/user')
  insertUser({ body }) {
    if (!body.name) {
      return Promise.reject('please input a user name')
    }
    const user = users.filter(item => item.name === body.name)[0]

    if (user) {
      return Promise.reject('this user is created, please recheck')
    }

    // insert to db
    lastCreateUserId++
    const insertItem = {
      id: lastCreateUserId,
      name: body.name
    }
    users.push(insertItem)

    return insertItem
  }

  @Delete('/user/:id')
  deleteUserById({ params }) {
    if (!params.id) {
      return Promise.reject('please input user id')
    }
    const userIndex = users.map(item => item.id).indexOf(parseInt(params.id))

    if (userIndex === -1) {
      return Promise.reject('this user is unexsist, please recheck')
    }

    //delete
    const deleteItem = users.splice(userIndex, 1)[0]

    return deleteItem
  }

  @Put('/user/:id')
  modifyUserById({ params, body }) {
    if (!params.id) {
      return Promise.reject('please input user id')
    }
    const userIndex = users.map(item => item.id).indexOf(parseInt(params.id))

    if (userIndex === -1) {
      return Promise.reject('this user is unexsist, please recheck')
    }

    if (!body.name) {
      return Promise.reject('please input a user name')
    }

    // update
    const updateItem = {
      ...users[userIndex],
      name: body.name
    }
    users.splice(userIndex, 1, updateItem)[0]

    return updateItem
  }
}

const service = new Service()

service.start()

// >>>>> curl "localhost:3000/user" -X GET
// <<<<< {"status":200,"result":[],"message":"OK"}

// >>>>> curl "localhost:3000/user/1" -X GET
// <<<<< {"status":400,"message":"this user is unexsist, please recheck"}

// >>>>> curl "localhost:3000/user" -X POST
// <<<<< {"status":400,"message":"please input a user name"}

// >>>>> curl "localhost:3000/user" -X POST -H "Content-type: application/json" -d'{"name": "leo"}'
// <<<<< {"status":200,"result":{"id":1,"name":"leo"},"message":"OK"}

// >>>>> curl "localhost:3000/user" -X POST -H "Content-type: application/json" -d'{"name": "leo"}'
// <<<<< {"status":400,"message":"this user is created, please recheck"}

// >>>>> curl "localhost:3000/user" -X POST -H "Content-type: application/json" -d'{"name": "tony"}'
// <<<<< {"status":200,"result":{"id":2,"name":"tony"},"message":"OK"}

// >>>>> curl "localhost:3000/user" -X GET
// <<<<< {"status":200,"result":[{"id":1,"name":"leo"},{"id":2,"name":"tony"} ],"message":"OK"}

// >>>>> curl "localhost:3000/user/1" -X GET
// <<<<< {"status":200,"result":{"id":1,"name":"leo"},"message":"OK"}

// >>>>> curl "localhost:3000/user/1" -X DELETE
// <<<<< {"status":200,"result":{"id":1,"name":"leo"},"message":"OK"}

// >>>>> curl "localhost:3000/user/1" -X GET
// <<<<< {"status":400,"message":"this user is unexsist, please recheck"}

// >>>>> curl "localhost:3000/user/2" -X PUT -H "Content-type: application/json" -d'{"name": "tom"}'
// <<<<< {"status":200,"result":{"id":2,"name":"tom"},"message":"OK"}

// >>>>> curl "localhost:3000/user" -X GET
// <<<<< {"status":200,"result":[{"id":2,"name":"tom"}],"message":"OK"}

# Binding method

When this method is bound to the specified route, the corresponding request will be responded to by this method, and the context of each request will be injected in the form of parameters. All parameters:

  • req: context.req
  • res: context.res
  • next: context.next
  • query: context.req.query
  • body: context.req.body
  • headers: context.req.headers
  • params: context.req.params All parameters will be injected into a context object and injected into the first parameter of the method.
@Server()
class Service {
  @Post('/all_arg')
  getData({ res, req, next, headers, query, body, params }) {
    // response with res.json
    res.json({ status: 2000, message: 'OK' })
  }
}

# Paramters

Paramters decorator is used to add parameter filters to the request, including the following:

  • Body body filter
  • Query query filter
  • Params params filter
  • Headers headers filter

# Validator

The first parameter represents the validator, required, which supports the following types:

  • The function parameter is the corresponding request content, please see the example:
const { Server, Get, Query, Body } = require('@totea/core')

@Server()
class Service {
  @Get()
  @Query(query => query.id && query.id.length === 10) // When req.query.id exists and the length is 10, it returns true, indicating that the verification passed, and if it fails, you will receive {status : 400, message: "Bad Request"}
  @Body(body => {
    // Return string format, indicating error message, you will receive {status: 400, message: "please provide a name"}
    if (!body.name) return 'please provide a name'
    // No return or return true, indicating that the test passed
  })
  @Params(params => {
    // Return Error, indicating that http error will receive {status: 400, message: error.message"}
    if (!params.address) return new Error()
  })
  @Headers(headers => {
    // Return number format, which means http error status, you will receive {status: 401, message: "Unauthorized"}
    if (!headers.token) return 401
  })
  getData() {}
}

Tegund is used extensively in totea as a dynamic parameter verification tool, which is also very useful in regular request parameter verification.

In addition, the totea framework itself depends on tegund, which means that you don’t need to install it separately, you can quote it directly, please see the example:

const { object, string, integer } = require('tegund')
const { Server, Body, Query, Body } = require('@totea/core')

@Server()
class Service {
  @Get()
  @Body(
    object({
      name: string()
        .min(2)
        .max(10), // req.body.name must be a string and the length is 2-10
      age: integer().min(0) // req.body.age must be a non-negative integer
    })
  )
  getData() {}
}

tegund will verify the parameters and return the corresponding error message when the verification fails. You only need to provide a suitable verifier.

You can even omit object and directly:

@Body({
  name: string().min(2).max(10),
  age: integer().min(0)
})

For simpler cases, you can even do this:

@Body({
  name:'string',
  age:'integer'
})

For more information, please refer to tegund documentation (opens new window)

# Default error message

The second parameter is used to specify a default error message, optional, string type

const { Server, Get, Query, Body } = require('@totea/core')

@Server()
class Service {
  @Get()
  // If you fail, you will receive {status: 400, message: "expected a string id, length = 10"}
  @Query(
    query => query.id && query.id.length === 10,
    'expected a string id, length = 10'
  )
  getData() {}
}

The priority of the error message is the error message returned by the validator> the default error message specified by the second parameter> the error message corresponding to httpError (400 if it is not specified).

The source code is as follows:

// result represents the return value of the validator errorMessage represents the default error message provided
if (result === false) {
  throw createHttpError(400, errorMessage)
}

if (result instanceof Error) {
  throw createHttpError(400, result.message || errorMessage)
}

if (typeof result === 'string') {
  throw createHttpError(400, result || errorMessage)
}

if (typeof result === 'number') {
  throw createHttpError(result, errorMessage)
}

# Middleware

Middleware decorator is used to add global middleware to Server and Controller, or to add separate middleware to a request.

Middleware is the core of express, which is also important in totea. You may have discovered that the Paramters parameter validator mentioned above is actually a special kind of middleware, isn't it?

# Global Middleware

const { Server, Middleware } = require('@totea/core')

@Server()
@Middleware((req, req, next) => {
  // The first global middleware
  console.log('call first global middleware of service')
  next()
})
@Middleware((req, req, next) => {
  // Add another global middleware, which will be executed after the first middleware is executed
  console.log('call second global middleware of service')
  next()
})
class ToteaServer {}

You can also add to the controller:

const { Controller, Middleware } = require('@totea/core')

@Controller('child')
@Middleware((req, req, next) => {
  // The first sub-route global middleware
  console.log('call first global middleware of service')
  next()
})
@Middleware((req, req, next) => {
  // Add another sub-route global middleware, which will be executed after the first middleware is executed
  console.log('call second global middleware of service')
  next()
})
class ChildController {}

# Private middleware

You can add private middleware to a single route:

const { Server, Middleware } = require('@totea/core')

@Server()
class ToteaServer {
  @Get('/user')
  @Middleware((req, req, next) => {
    // The first private middleware
    console.log('call first middleware of /user')
    next()
  })
  @Middleware((req, req, next) => {
    // Add another private middleware, which will be executed after the first middleware is executed
    console.log('call second middleware of /user')
    next()
  })
  getUser() {}
}

# Using Express middleware

As we said, totea uses express as a web server, we have not modified any underlying logic, which means that all middleware developed for Express can be used directly

Example, use morgan to print the request log:

const { Server, Middleware } = require('@totea/core')
const morgan = require('morgan')

@Server()
@Middleware(morgan('combined'))
class ToteaServer {}

# Built-in log middleware

Of course, totea also comes with a simple log printing middleware, how to use it:

const {Server, Logger, Get} = require('@totea/core')
const morgan = require('morgan')

@Server()
@Logger()
class Service{
  @Get('/user')
  getUser() {
    return {
      result: {"docs":[],"count":0,"page":1,"limit":10},
      message:'user query success!'
    }
  }
}

// logs
[totea logger]: 2021-04-07T09:43:18.836Z /user GET {"status":200,"result":{"docs":[],"count":0,"page":1," limit":10},"message":"user query success!"} 11ms

[totea logger]: 2021-04-07T09:45:07.921Z /user GET {"status":200,"result":{"docs":[],"count":0,"page":1," limit":10},"message":"user query success!"} 3ms

[totea logger]: 2021-04-07T09:45:08.684Z /user GET {"status":200,"result":{"docs":[],"count":0,"page":1," limit":10},"message":"user query success!"} 1ms

[totea logger]: 2021-04-07T09:45:09.346Z /user GET {"status":200,"result":{"docs":[],"count":0,"page":1," limit":10},"message":"user query success!"} 1ms

# Successful response

In express, generally use req.send or res.json to respond to requests. This method is also applicable in totea:

@Server()
class Service {
  @Get('/user')
  getUser({ res }) {
    res.status(200).json({ status: 200, message: 'OK' })
  }

  @Get('/page')
  getPage({ res }) {
    res.send('<p>some html</p>')
  }

  @Get('/html')
  getPage({ res }) {
    res.sendFile('test.html', { root: 'pages' })
  }
}

In totea, we have a more convenient way to return json, just put the content in the return value of the function. For requests other than json, the res method still needs to be used to respond.

Note that when the function does not return a value, or returns undefined (which cannot be distinguished in the actual code), totea will treat the request as a correct response and return {status: 500, message: "Internal Server Error"}

@Server()
class Service{
@Get('/user')
getUser() {// The returned content will be wrapped in the form of {status: 200, message: "OK", result: ${return}}
return {name:'leo', address:'XXX'}
}

@Get('/json')
getPage() {
// Assuming the return is an obejct, and has an integer type status, or there is a message with a non-empty string format, return the json
return {
status: 200,
result: {name:'leo', address:'XXX' },
message:'OK'
}
}
}

// Both of the above two requests will return the same content:
{
status: 200,
result: {name:'leo', address:'XXX' },
message:'OK'
}

totea hijacks the sendFile and send methods of res in order to obtain the expected return content before requesting a response. At the same time, we have also avoided the problem of repeated responses to requests.

In the following example, after the request is responded to by res.json, although the code will continue to run down, it will not respond repeatedly. Of course, it is a good coding practice to return in time.

@Server()
class Service {
  @Get('/user')
  getUser({ res, query }) {
    if (!query.id) {
      res.json({ status: 404, message: 'please provide a user id' })
    }
    return { name: 'leo', address: 'XXX' }
  }
}

# Failed response

Compared with a successful response, in actual coding, the interface returns an error situation is more common, totea provides a variety of ways to deal with:

Use http-errors (opens new window) in totea to create httpError, you can import the library directly, or use the createHttpError method directly.

const { Server, Get, createHttpError } = require('@totea/core')
@Server()
class Service {
  // response by res, got: {"status":401,"message":"Unauthorized"}
  @Get('/error_res')
  errorRes({ res }) {
    res.json({ status: 401, message: 'Unauthorized' })
  }

  // return a Promise.reject, with a http status, got: {"status":404,"message":"Not Found"}
  @Get('/error_status')
  errorstatus() {
    return Promise.reject(404)
  }

  // return a Promise.reject, with a error message, got: {"status":400,"message":"this is the invalid message"}
  @Get('/error_message')
  errorMessage() {
    return Promise.reject('this is the invalid message')
  }

  // return a Promise.reject, with http status and message, got: {"status":410,"message":"this is the invalid message"}
  @Get('/error_status_message')
  errorstatusAndMessage() {
    return Promise.reject({
      status: 410,
      message: 'this is the invalid message'
    })
  }

  // return a http error, got: {"status":401,"message":"Unauthorized"}
  @Get('/http_error')
  httpError() {
    return createHttpError(401)
  }

  // throw a http error, got: {"status":401,"message":"Unauthorized"}
  @Get('/http_error')
  throwHttpError() {
    throw createHttpError(401)
  }

  // return a normal error, got {"status":406,"message":"this is a error message"}
  @Get('/return_simple_error')
  returnSimpleError() {
    const e = new Error('this is a error message')

    e.status = 406

    return e
  }

  // throw a normal error, got {"status":406,"message":"this is a error message"}
  @Get('/throw_simple_error')
  throwSimpleError() {
    const e = new Error('this is a error message')

    e.status = 406

    throw e
  }
}

Express does not provide a global error handling method. It is particularly difficult to intercept exceptions thrown by async/await. Totea uses [express-async-errors](https://www.npmjs.com/package/express-async- errors), when an unknown error is intercepted, a {status: 500, message: "Internal Server Error"} is always returned.

# Route priority

Express itself does not provide routing priority ordering, the order of routing depends on your code order. When using express native methods to define routes, your app may have inaccessible dead zones:

const express = require('express')

const app = express()

app.get('/:id', (req, res) => {
  // All requests are responded here
  res.json({ id: req.params.id })
})

app.get('/user', (req, res) => {
  // dead zone
  res.json({ user: 'leo' })
})

app.listen(3000)

This is inconsistent with what we expected. We hope that specific routes will match first, followed by matching routes.

There is no need to worry about this situation in totea. We use sort-route-addresses (opens new window) to sort the priority of routes by default:

const { Server, Get } = require('@totea/core')

@Server()
class Service {
  @Get('/:id')
  getUserById({ res, params }) {
    res.json({ id: params.id })
  }

  @Get('/user')
  getUser({ res }) {
    res.json({ user: 'leo' })
  }
}

const service = new Service()

service.start()

This example is exactly the same as above, but meets the requirements.

# HTTP status code

The original intention of totea is to create an API server, and it will be very efficient to use it to write interfaces.

In the actual development process, our interface may be required to always return data with a 200 status code, and the actual status information is reflected in the returned json. For example, a client development engineer may ask you to design an interface like this:

// When the request is successful
Status status: 200 OK
response: {status: 200, message: "OK", result: [{ name:'leo' }]}
// When there is an error in the request, such as insufficient permissions
Status status: 200 OK
response: {status: 401, message: "Unauthorized"}

It must be said that this is also a norm, but it is contrary to mainstream design ideas, such as RESTful:

// When the request is successful
Status status: 200 OK
response: {result: [{ name:'leo' }]}
// When there is an error in the request, such as insufficient permissions
Status status: 401 Unauthorized
response: null

There have been heated discussions on this issue on V2EX, the original address: API uses HTTP status code or all return 200 (opens new window), each developer has different understanding

Totea uses the first specification by default, but allows developers to customize the response. The Server decorator accepts an onResponse method, which will be called before each request is returned. You can provide a custom method to override it.

The default onResponse method is:

function onResponse({ res, status, result, message }) {
  res.json(
    removeEmpty(
      {
        // if some arg is undefined, will remove it
        status: status,
        message: message,
        result
      },
      {
        removeNull: false,
        removeUndefined: true
      }
    )
  )
}

Suppose you want to use the RESTful specification:

@Server({
  onResponse: ({ res, status, result, message }) => {
    res.status(status)
    res.json({ result, message })
  }
})
class Service {}

# HTML templates and static directories

For detailed examples, please see Address (opens new window).

# Similar framework

  • nestjs link (opens new window) nestjs is a large and comprehensive framework with excellent design ideas. It needs to be used with typescript, which is suitable for developing large-scale projects.
  • overnightjslink (opens new window) The design idea of ​​overnightjs is similar to totea, the difference is that it needs to be used with typescript.