Routing and controller
In the common MVC architecture, C represents the controller, which is responsible for parsing the user's input and returning the corresponding results after processing.
As shown in the figure, the client requests the controller of the server through the Http protocol, and the controller responds to the client after processing. This is the most basic "request-response" process.
Common ones are:
- In the RESTful interface, the controller accepts the user's parameters, returns the contents from the database to the user, or updates the user's request to the database.
- In the HTML page request, the controller renders different templates to obtain HTML and returns it to the user according to the user's access to different URLs.
- In a proxy server, the controller forwards the user's request to other servers and returns the processing results of other servers to the user.
Generally speaking, the controller is often used to verify, convert, and call complex business logic on the user's request parameters, assemble the data after getting the corresponding business results, and then return.
In Midway, controllers also carry routing capabilities. Each controller can provide multiple routes, and different routes can perform different operations.
In the following example, we will demonstrate how to create a route in the controller.
Routing
Controller files are generally in the src/controller
directory, where we can create controller files. Midway annotates controllers with the @Controller()
decorator, where the decorator has an optional parameter for route prefix (grouping), so that all routes under this controller will carry this prefix.
At the same time, Midway provides a method decorator for marking the type of request.
For example, we create a homepage controller to return a default/
route.
➜ my_midway_app tree
.
├── src
│ └── controller
│ └── home.ts
├── test
├── package.json
└── tsconfig.json
// src/controller/home.ts
import { Controller, Get } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Get('/')
async home() {
return "Hello Midwayjs!";
}
}
The @Controller
decorator tells the framework that this is a class of type Web Controller, and the @Get
decorator tells the framework that the decorated home
method will be exposed as a /
route, which can be accessed by GET
request to access.
The whole method returns a string, and in the browser you will receive the response type of text/plain
and a 200
status code.
Routing methods are all async methods.
Routing method
In the preceding example, you have created a GET route. In general, we will have other HTTP Methods, Midway provides more routing method decorators.
// src/controller/home.ts
import { Controller, Get, Post } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Get('/')
async home() {
return 'Hello Midwayjs!';
}
@Post('/update')
async updateData() {
return 'This is a post method'
}
}
Midway also provides other decorators, such as @Get
, @Post
, @Put()
, @Del()
, @Patch()
, @Options()
, @Head()
, and @All()
.
The @All
decorator is special, indicating that it can accept all types of HTTP methods.
You can bind multiple routes to the same method.
@Get('/')
@Get('/main')
async home() {
return 'Hello Midwayjs!';
}
Get request parameters
Next, we will create an HTTP API about users. Similarly, we will create a src/controller/user.ts
file. This time we will add a routing prefix and more request types.
Let's take the user type as an example, first add a user type, and we usually put the defined content in the src/interface.ts
file.
➜ my_midway_app tree
.
├── src
│ ├── controller
│ │ ├── user.ts
│ │ └── home.ts
│ └── interface.ts
├── test
├── package.json
└── tsconfig.json
// src/interface.ts
export interface User {
id: number;
name: string;
age: number;
}
Add a route prefix and the corresponding controller.
// src/controller/user.ts
import { Controller } from '@midwayjs/core';
@Controller('/api/user')
export class UserController {
// xxxx
}
Next, we will call different processing logic for different request types. Except for the request type, the requested data is generally dynamic and will be passed at different locations in HTTP, such as common Query,Body, etc.
Decorator parameter conventions
Midway adds a common decorator for dynamic values. Take the @Query
decorator as an example. The @Query
decorator obtains the query parameters in the URL and assigns them to the input parameters of the function. In the following example, the id is obtained from the query parameter of the route. If the URL is /?id = 1
, the value of the id is 1. At the same time, the route returns an object of the User
type.
// src/controller/user.ts
import { Controller, Get, Query } from '@midwayjs/core';
@Controller('/api/user')
export class UserController {
@Get('/')
async getUser(@Query('id') id: string): Promise<User> {
// xxxx
}
}
The @Query
decorator has a parameter. You can pass in a specified string key to obtain the corresponding value and assign the value to the input parameter. If the parameter is not passed in, the entire query object is returned by default.
// URL = /?id=1
async getUser(@Query('id') id: string) // id = 1
async getUser(@Query() queryData) // {"id": "1"}
Midway provides more decorators that get values from Query, Body, Header, etc., which are all out of the box and adapted to different upper-level Web frameworks.
The following are these decorators and the corresponding equivalent frame values.
Decorator | Express the corresponding method | Corresponding method of Koa/EggJS |
---|---|---|
@Session(key?: string) | req.session / req.session [key] | ctx.session / ctx.session [key] |
@Param(key?: string) | req.params / req.params [key] | ctx.params / ctx.params [key] |
@Body(key?: string) | req.body / req.body [key] | ctx.request.body / ctx.request.body [key] |
@Query(key?: string) | req.query / req.query [key] | ctx.query / ctx.query[key] |
@Queries(key?: string) | - | -/ctx.queries [key] |
@Headers(name?: string) | req.headers / req.headers [name] | ctx.headers / ctx.headers [name] |
Note @Queries decorator is different from @Query.
Queries will aggregate the same keys together and become an array. When the interface parameter accessed by the user is /? When name = a & name = B
, @Queries will return {name: [a, B] }
, while Query will only return {name: B}
.
Query
The part after ?
in the URL is a Query String, which is often used to pass parameters in GET type requests.
For example
GET /user?uid=1&sex=male
It is the parameter passed by the user.
Example: Get from Decorator
// src/controller/user.ts
import { Controller, Get, Query } from '@midwayjs/core';
@Controller('/user')
export class UserController {
@Get('/')
async getUser(@Query('uid') uid: string): Promise<User> {
// xxxx
}
}
Example: Get from an API operation
// src/controller/user.ts
import { Controller, Get, Inject } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Controller('/user')
export class UserController {
@Inject()
ctx: Context;
@Get('/')
async getUser(): Promise<User> {
const query = this.ctx.query;
// {
// uid: '1',
// sex: 'male',
//}
}
}
Note EggJS is different from other frameworks. When the key in the Query String is repeated, ctx.query
only takes the value of the first occurrence of the key, and subsequent occurrences will be ignored.
For example, the value obtained by GET /user?uid=1&uid=2
through ctx.query
is { uid: '1' }
.
Body
Although we can pass parameters through the URL, there are still many limitations:
- The length of the URL is limited in the browser. If there are too many parameters to pass, the URL cannot be passed.
- The server often records the complete URL of the access to the log file, and it is unsafe to pass some sensitive data through the URL.
In the previous HTTP request message example, we saw that there is a body part after the header, and we usually pass the parameters of POST, PUT, DELETE and other methods in this part. When there is a body in a general request, the client (browser) will send a Content-Type
at the same time to tell the server what format the body of this request is. The two most commonly used data delivery formats in Web development are JSON
and Form
.
The framework has built-in bodyParser middleware to parse requests in these two formats into objects and mount them to ctx.request.body
. HTTP protocol does not recommend passing body when accessing through GET and HEAD methods, so we cannot obtain content according to this method in GET and HEAD methods.
Example: Get a single body
// src/controller/user.ts
// POST /user/ HTTP/1.1
// Host: localhost:3000
// Content-Type: application/json; charset=UTF-8
//
// {"uid": "1", "name": "harry"}
import { Controller, Post, Body } from '@midwayjs/core';
@Controller('/user')
export class UserController {
@Post('/')
async updateUser(@Body('uid') uid: string): Promise<User> {
// id is equivalent to ctx.request.body.uid
}
}
Example: Get the entire body
// src/controller/user.ts
// POST /user/ HTTP/1.1
// Host: localhost:3000
// Content-Type: application/json; charset=UTF-8
//
// {"uid": "1", "name": "harry"}
import { Controller, Post, Body } from '@midwayjs/core';
@Controller('/user')
export class UserController {
@Post('/')
async updateUser(@Body() user: User): Promise<User> {
// user is equivalent to the entire body object of ctx.request.body
// => output user
// {
// uid: '1',
// name: 'harry',
// }
}
}
Example: Get from an API operation
// src/controller/user.ts
// POST /user/ HTTP/1.1
// Host: localhost:3000
// Content-Type: application/json; charset=UTF-8
//
// {"uid": "1", "name": "harry"}
import { Controller, Post, Inject } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Controller('/user')
export class UserController {
@Inject()
ctx: Context;
@Post('/')
async getUser(): Promise<User> {
const body = this.ctx.request.body;
// {
// uid: '1',
// name: 'harry',
//}
}
}
Example: Obtain query and body parameters
Decorators can be used in combination.
@Post('/')
async updateUser(@Body() user: User, @Query('pageIdx') pageIdx: number): Promise<User> {
// user gets it from body
// pageIdx obtained from query
}
The framework sets some default parameters for the bodyParser. After configuration, it has the following features:
- If the request Content-Type is
application/json
,application/json-patch + json
,application/vnd.api + json
, andcsp-report
, the request body is parsed in json format and the maximum length of the body is limited to1mb
. - When the request Content-Type is
application/x-www-form-urlencoded
, the request body is parsed in the form format and the maximum length of the body is limited to1mb
. - If the parsing is successful, the body must be an Object (possibly an array).
Common errors: ctx.request.body
is confused with ctx.body
, which is the abbreviation of ctx.response.body
.
Router Params
If the route is declared in the :xxx
format, you can use ctx.params
to obtain the parameters.
Example: Get from Decorator
// src/controller/user.ts
// GET /user/1
import { Controller, Get, Param } from '@midwayjs/core';
@Controller('/user')
export class UserController {
@Get('/:uid')
async getUser(@Param('uid') uid: string): Promise<User> {
// xxxx
}
}
Example: Get from an API operation
// src/controller/user.ts
// GET /user/1
import { Controller, Get, Inject } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Controller('/user')
export class UserController {
@Inject()
ctx: Context;
@Get('/:uid')
async getUser(): Promise<User> {
const params = this.ctx.params;
// {
// uid: '1',
//}
}
}
Header
In addition to getting parameters from the URL and request body, many parameters are passed through the request header. The framework provides some auxiliary properties and methods to obtain.
ctx.headers
,ctx.header
,ctx.request.headers
,ctx.request.header
: These methods are equivalent to obtaining the entire header object.ctx.get(name)
,ctx.request.get(name)
: Gets the value of a field in the request header. If this field does not exist, an empty string is returned.- We recommend using
ctx.get(name)
instead ofctx.headers['name']
, because the former will automatically handle case.
Example: Get from Decorator
// src/controller/user.ts
// GET /user/1
import { Controller, Get, Headers } from '@midwayjs/core';
@Controller('/user')
export class UserController {
@Get('/:uid')
async getUser(@Headers('cache-control') cacheSetting: string): Promise<User> {
// no-cache
// ...
}
}
Example: Get from an API operation
// src/controller/user.ts
// GET /user/1
import { Controller, Get, Inject } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Controller('/user')
export class UserController {
@Inject()
ctx: Context;
@Get('/:uid')
async getUser(): Promise<User> {
const cacheSetting = this.ctx.get('cache-control');
// no-cache
}
}
Cookie
HTTP requests are stateless, but our Web applications usually need to know who initiated the request. To solve this problem, HTTP protocol designs a special request header: Cookie. The server can respond to a small amount of data to the client through the response header (set-cookie). The browser will follow the protocol to save the data and bring it with it when requesting the same service next time (the browser will also follow the protocol and only bring the corresponding cookie when visiting websites that meet the cookie specified rules to ensure security).
ctx.cookies
allows us to set and read Cookie conveniently and safely in our Controller.
import { Inject, Controller, Get, Provide } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Controller('/')
export class HomeController {
@Inject()
ctx: Context;
@Get('/')
async home() {
// set cookie
this.ctx.cookies.set('foo', 'bar', { encrypt: true });
// get cookie
this.ctx.cookies.get('foo', { encrypt: true });
}
}
Although a cookie is only a header in HTTP, you can set multiple key-value pairs in the format of foo = bar;foo1 = bar1;
.
Cookie often plays a role in transmitting client identity information in Web applications. Therefore, there are many security-related configurations that cannot be ignored. The Cookie document describes the usage of cookies and security-related configuration items in detail.
Session
Through Cookie, we can set a Session for each user to store information related to the user's identity. This information will be encrypted and stored in Cookie to maintain the user's identity across requests.
The framework has built-in Session plug-ins, which provide us with ctx.session
to access or modify the current user Session.
import { Inject, Controller, Get, Provide } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Controller('/')
export class HomeController {
@Inject()
ctx: Context;
@Get('/')
async home() {
// Get the content on the Session
const userId = this.ctx.session.userId;
const posts = await this.ctx.service.post.fetch(userId);
// Modify the value of the Session
this.ctx.session.visited = ctx.session.visited? (ctx.session.visited + 1) : 1;
// ...
}
}
The use of the Session is very intuitive, just read it directly or modify it. If you want to delete it, assign it null
directly:
ctx.session = null;
Like Cookie, Session also has many security options and functions. It is better to read [Session](cookie_session# Default-session) documents for further understanding before using them.
Uploaded file
Generally, the multipart/form-data
protocol header is used to obtain the uploaded files by the @Files
decorator. The upload function is provided by the upload component. For more information, see upload component.
Other parameters
There are also some more common parameter decorators and their corresponding methods.
Decorator | Express the corresponding method | Corresponding method of Koa/EggJS |
---|---|---|
@RequestPath | req.baseurl | ctx.path |
@RequestIP | req.ip | ctx.ip |
Example: Obtain the body, path, and ip address
@Post('/')
async updateUser (
@Body('id') id: string,
@RequestPath() p: string
@RequestIP() ip: string): Promise<User> {
}
Custom request parameter decorator
You can quickly create custom request parameter decorators with createRequestParamDecorator
.
import { createRequestParamDecorator } from '@midwayjs/core';
// Implement decorator
export const Token = () => {
return createRequestParamDecorator(ctx => {
return ctx.headers.token;
});
};
// Use decorator
export class UserController {
async invoke(@Token() token: string) {
console.log(token);
}
}
Request parameter type conversion
If it is a simple type, Midway will automatically convert the parameter to the user-declared type.
For example:
Number type
@Get('/')
async getUser(@Query('id') id: number): Promise<User> {
console.log(typeof id) // number
}
Boolean type
- When the value is 0,"0", "false" is converted to false, and the rest return Boolean(value) values
@Get('/')
async getUser(@Query('id') id: boolean): Promise<User> {
console.log(typeof id) // boolean
}
If it is a complex type, if the specified type is Class, it will be automatically converted to an instance of the class.
// class
class UserDTO {
name: string;
getName() {
return this.name;
}
}
@Get('/')
async getUser(@Query() query: UserDTO): Promise<User> {
// query.getName()
}
If you do not want to be converted, you can use Interface.
interface User {
name: string;
}
@Get('/')
async getUser(@Query() query: User): Promise<User> {
// ...
}
Parameter verification
The parameter verification function is provided by the validate component. For details, please refer to the validate component.
Set HTTP response
Set the return value
Most of the data is sent to the requester through the body. Like the body in the request, the body sent in the response also needs a matching Content-Type to tell the client how to parse the data.
- As a RESTful API interface controller, we usually return a body with Content-Type in
application/json
format and the content is a JSON string. - As a controller of an html page, we usually return a body with a Content-Type of
text/html
format, and the content is html code segments.
In Midway, you can simply use return
to return data.
import { Controller, Get, HttpCode } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Inject()
ctx: Context;
@Get('/')
async home() {
// Return string
return "Hello Midwayjs!";
// Return json
return {
a: 1
b: 2
};
// return html
return '<html><h1>Hello</h1></html>';
// Return to stream
return fs.createReadStream('./good.png');
}
}
You can also use koa's native API.
import { Controller, Get, HttpCode } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Get('/')
async home() {
// Return string
this.ctx.body = "Hello Midwayjs!";
// Return JSON
this.ctx.body = {
a: 1,
b: 2,
};
// return html
this.ctx.body = '<html><h1>Hello</h1></html>';
// Return to stream
this.ctx.body = fs.createReadStream('./good.png');
}
}
Note: ctx.body
is the abbreviation of ctx.response.body
. Do not confuse it with ctx.request.body
.
Set status code
By default, the status code of the response is always 200, and we can easily change this behavior by adding a @HttpCode
decorator at the handler layer or through the API.
When sending an error, such as 4xx/5xx
, you can use exception handling to throw an error.
Example: Using a Decorator
import { Controller, Get, HttpCode } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Get('/')
@HttpCode(201)
async home() {
return "Hello Midwayjs!";
}
}
Example: API operation
import { Controller, Get, Inject } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Inject()
ctx: Context;
@Get('/')
async home() {
this.ctx.status = 201;
// ...
}
}
The status code cannot be modified after the response stream is closed (after response.end).
Set response header
Midway provides a @SetHeader
decorator or an API to simply set up a custom response header.
Example: Using a Decorator
import { Controller, Get, SetHeader } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Get('/')
@SetHeader('x-bbb', '123')
async home() {
return "Hello Midwayjs!";
}
}
When there are multiple response headers that need to be modified, you can directly pass in the object.
import { Controller, Get, SetHeader } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Get('/')
@SetHeader({
'x-bbb ': '123',
'x-ccc ': '234'
})
async home() {
return "Hello Midwayjs!";
}
}
Example: API operation
import { Controller, Get, Inject } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Inject()
ctx: Context;
@Get('/')
async home() {
this.ctx.set('x-bbb', '123');
// ...
}
}
The response header cannot be modified after the response flow is closed (after response.end).
Redirection
If you need to simply redirect a route to another route, you can use the @Redirect
decorator. The parameters of the @Redirect
decorator are a redirect URL and an optional status code. The default redirect status code is 302
.
In addition, you can jump through the API.
Example: Using a Decorator
import { Controller, Get, Redirect } from '@midwayjs/core';
@Controller('/')
export class LoginController {
@Get('/login_check')
async check() {
// TODO
}
@Get('/login')
@Redirect('/login_check')
async login() {
// TODO
}
@Get('/login_another')
@Redirect('/login_check', 302)
async loginAnother() {
// TODO
}
}
Example: API operation
import { Controller, Get, Inject } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Inject()
ctx: Context;
@Get('/')
async home() {
this.ctx.redirect('/login_check');
// ...
}
}
Redirection cannot be modified after the response flow is closed (after response.end).
Response type
Although the browser will automatically judge the best response content based on the content, we often encounter situations that need to be set manually. We also provide a @ContentType
decorator for setting the response type.
In addition, it can also be set through API.
Example: Using a Decorator
import { Controller, Get, ContentType } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Get('/')
@ContentType('html')
async login() {
return '<body>hello world</body>';
}
}
Example: API operation
import { Controller, Get, Inject } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Inject()
ctx: Context;
@Get('/')
async home() {
this.ctx.type = 'html';
// ...
}
}
The response type cannot be modified after the response flow is closed (after response.end).
Streaming response
If you want to stream data back, you can use the write
and end
methods on the Node.js raw response object.
import { Controller, Get, Inject, sleep } from '@midwayjs/core';
import { Context } from '@midwayjs/koa';
@Controller('/')
export class HomeController {
@Inject()
ctx: Context;
@Get('/')
async home() {
this.ctx.status = 200;
this.ctx.set('Transfer-Encoding', 'chunked');
for (let i = 0; i < 100; i++) {
await sleep(100);
this.ctx.res.write('abc'.repeat(100));
}
this.ctx.res.end();
}
}
Internal redirection
Starting from v3.12.0, the framework provides an internal redirect API ctx.forward(url)
, which only supports koa/egg type.
The difference from external redirection is that internal redirection does not modify the URL of the browser, but only flows inside the program.
import { Controller, Get, Inject } from '@midwayjs/core';
@Controller('/')
export class HomeController {
@Inject()
ctx: Context;
@Get('/')
async home() {
return this.ctx.forward('/api');
}
@Get('/api')
async api() {
return 'abc';
}
}
Note that there are some rules for internal redirects:
-
- Redirection will retain all parameters of the original route, that is, transparently transmit the entire ctx
-
- Redirection can only be done in the same http method
-
- Redirection will not execute Web Middleware again, and guards will not be executed, but interceptors and parameter decorators will be executed
Global route prefix
It needs to be set in the src/config/config.default
configuration.
Note that different components are configured under different keywords:
- koa
- Egg.js
- Express
// src/config/config.default.ts
export default {
koa: {
globalPrefix: '/v1'
}
};
// src/config/config.default.ts
export default {
egg: {
globalPrefix: '/v1'
}
};
// src/config/config.default.ts
export default {
express: {
globalPrefix: '/v1'
}
};
After configuration, all routes automatically add this prefix.
If there are special routes that are not required, you can use the decorator parameters to ignore them.
Example: Controller Level Ignoring
// All routes under this Controller will ignore the global prefix
@Controller('/api', {ignoreGlobalPrefix: true})
export class HomeController {
// ...
}
Example: route level ignored
@Controller('/')
export class HomeController {
// This route will not be ignored
@Get('/', {})
async homeSet() {
}
// The route ignores the global prefix
@Get('/bbc', {ignoreGlobalPrefix: true})
async homeSet2() {
}
}
Routing priority
Midway has already sorted the routes uniformly, and the wildcard route will automatically reduce the priority and be loaded at the end.
The rules are as follows:
-
- The absolute path rule has the highest priority, such as
/AB/cb/e
.
- The absolute path rule has the highest priority, such as
-
- The asterisk can only appear last and must be followed by/. For example,
/AB/cb/**
- The asterisk can only appear last and must be followed by/. For example,
-
- If the absolute path and the general configuration can match one path, the absolute rule has a high priority, such as
/abc/*
and/abc/d
, then the next absolute route is matched when the/abc/d
request is requested.
- If the absolute path and the general configuration can match one path, the absolute rule has a high priority, such as
-
- If multiple wildspaces match a path, the longest rule matches. For example,
/AB/**
and/AB/cd/**
hit/AB/cd/**
when matching/AB/cd/f
.
- If multiple wildspaces match a path, the longest rule matches. For example,
-
- If both
/
and/*
match/
, the priority of/
is higher than that of/*
- If both
-
- If the weights are the same, such as
/:page/page
and/page/:page
.
- If the weights are the same, such as
This rule is also consistent with the routing rules of the functions under the Serverless.
It is simply understood as "clear routes have the highest priority, long routes have the highest priority, and general distribution has the lowest priority".
For example:
@Controller('/api')
export class APIController {
@Get('/invoke/*')
async invokeAll() {
}
@Get('/invoke/abc')
async invokeABC() {
}
}
In this case, /invoke/abc
is registered first to ensure higher priority.
The priority of different Controller will be sorted by length, and the/
root Controller will be loaded finally.