Interface requests are no different from other resource requests. They all use HTTP protocol to return the corresponding resources. This article briefly introduces how Node develops interfaces and how to manage multiple interface situations and interface styles
The title is associated with Node, mainly because node is very simple to start a server, and the syntax is basically the same without too much burden, this article mainly explains the idea, conversion to other languages can also be.
Take a look at an example from the official website and modify it slightly so that it returns a fixed JSON data
const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) = > {
res.statusCode = 200;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.write(JSON.stringify({ name: 'hello wrold' }));
res.end();
});
server.listen(port, hostname, () = > {
console.log(The server runs in http://${hostname}:${port}/ `);
});
Copy the code
Copy the above code into a file and preview it with the help of Node xxx.js.
koa
The above is implemented with the help of the HTTP native module of Node. Of course, there is nothing wrong with this implementation, but for the purpose of extensibility and simplicity of development, KOA has been chosen as the framework to be used below.
Koa is touted as the next generation of Web development frameworks. Similarly, install koA and see how it implements the above functions
yarn add koa
Copy the code
const Koa = require('koa');
const hostname = '127.0.0.1';
const port = 3000;
const app = new Koa();
app.use(async (ctx) => {
ctx.type = 'application/json';
ctx.body = { name: 'hello wrold' };
});
app.listen(port, hostname, () = > {
console.log(The server runs in http://${hostname}:${port}/ `);
});
Copy the code
The code aspect is very concise, here mainly introduces the implementation idea but introduces koA syntax, and in fact KOA is only HTTP module encapsulation, the documentation is not much recommendation can see the official website.
When it comes tokoa
Let’s talk about it herekoa
Middleware, the following code is often used,koa
Using middleware to implement extensions is a plugin-like function, which itself is very much like an onion structure
For example, app.use above is middleware. The execution sequence of middleware is divided by next. First, the first half of next is executed, and then the next code of the last half is executed in a flashback structure
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(2);
});
app.use(async (ctx, next) => {
console.log(3);
await next();
console.log(4);
});
app.use(async (ctx, next) => {
console.log(5);
await next();
console.log(6);
});
Copy the code
The printed result of the above code is 1,3,5,6,4,2.
Interface development is generally through json message, koa grammar itself already very simple, but need to return to repeat every time, in the long run will certainly have a mistake or write misspelled, leakage and throw an error also needs to have a common method, the following is a return information and throw an error.
app.use(async (ctx) => {
ctx.sendData({ name: 'hello wrold' });
// If an error occurs
ctx.throwError('not satisfied with XXX');
});
Copy the code
It would be much easier if the code all came back this way, and the actual middleware could have problems, either handled by the KOA’s built-in listening errors, or wrapped in a try. As you can expect, managing each try manually would be frustrating.
With middleware mechanisms it’s easy to write a function with sendData and throwError that just returns in CTX and calls next for subsequent instances to execute
app.use(async (ctx, next) => {
ctx.sendData = () = > {};
ctx.throwError = () = > {};
await next();
});
Copy the code
The above example is simplified, but I’ll stagger it a little bit and go into more detail after the implementation
The order of the middleware is very important
The interface structure
There should be a sendData and throwError method to return information and throw an error. Here are the parameters and implementation of these two methods.
First the interface returns information, expecting it to be fixed into the following structure
{
"data": {},
"message": "ok"."code": 200
}
Copy the code
Message is optional. By default, you can give an OK and a code of 200. Here the code value is fixed and the method is not allowed to change
Error messages, expect it to be this structure
{
"message": ""."code": "400"
}
Copy the code
Here message is mandatory and code is optional.
So just a little bit about what’s wrong with code? Or is it by message? If you use code to distinguish different states, it is necessary to maintain a code list. In fact, this is very tedious and simple numeric memory does not meet human memory. However, using message to prompt you can basically guess the error situation, for example, you can return in this way
{
"message": "Error_ User name cannot be empty"
}
Copy the code
The front type behind the prompt, is not a lot of succinct, these two kinds of error prompt you can choose one.
Said the need to implement the function, method implementation is very simple, the following code is code value style implementation
// Ignore the top-level syntax, the implementation is extracted
async (ctx, next) => {
constcontent = { ... ctx,sendData: (data, message = 'ok') = > {
ctx.body = {
data,
message,
code: 200}; ctx.type ='application/json';
},
throwError: (message = 'wrong', code = 400) = > {
ctx.body = {
code,
message,
};
ctx.type = 'application/json'; }};try {
await callback(content);
} catch (e) {
ctx.body = {
code: 400.message: (e instanceof Error ? e.message : e) || 'System error'}; ctx.status =400;
}
await next();
};
Copy the code
rest
Rest is simply a set of rules for interfaces. It has the following rules
- use
get
To obtain resources - use
post
To send the request - use
put
To update resources - use
delete
To delete resources
So what are the benefits of using REST?
First, REST is just a specification, and defining a specification is easier to understand and read than a code specification
Automatic import
There must be different interfaces in the development of the project, how to manage these interfaces is very necessary, one by one import management is fine, but when the project is large enough, one by one adjustment as the business changes must be maddening.
The following uses koA-Router and middleware to write an automatic import interface function. First, let’s look at the simple use of koAR-Router
yarn add @koa/router
Copy the code
const Koa = require('koa');
const Router = require('@koa/router');
const hostname = '127.0.0.1';
const port = 3000;
const app = new Koa();
const router = new Router();
router.get('/'.(ctx, next) = > {
ctx.type = 'application/json';
ctx.body = { name: 'hello wrold' };
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(port, hostname, () = > {
console.log(The server runs in http://${hostname}:${port}/ `);
});
Copy the code
To implement this function, define the rules
-
Import only the interface files at the end of index.js in the SRC directory
To search for all the required index.js files, use the glob module with the wildcard ‘SRC /**/index.js’.
-
Import the file and add the fields returned by the template to the router
You can use node’s native require to read files, but it needs to be implemented with a little care. Modules must be formatted in order to be imported, and a try must be added to catch files that are not modules
Before implementing this function, it is important to specify the format of the module in the index.js file
const api = {
url: ' '.methods: 'get'| | ['post'].async callback(ctx){}};Copy the code
The format above is the convention, only to meet the structure will be imported in, because the development is using TS here will not do conversion JS operation, if you do not want to use TS directly ignore the type annotation to see about the implementation.
utils.ts
import glob from 'glob';
import path from 'path';
import _ from 'lodash';
import { Iobj, Istructure } from '.. /.. /typings/structure';
export const globFile = (pattern: string): Promise<Array<string>> => {
return new Promise((resolve, reject) = > {
glob(pattern, (err, files) = > {
if (err) {
return reject(err);
}
return resolve(files);
});
});
};
export const importModule = async() = > {const pattern = 'src/**/index.ts';
const list = await globFile(pattern);
const listMap = list.map((item) = > {
const f = path.resolve(process.cwd(), item);
return import(f)
.then((res) = > {
// Filter out attributes of default, return others
return _.omit(res, ['default']);
})
.catch(() = > null);
});
return (await Promise.all(listMap)).filter((f) = > f) as Array<Iobj<Istructure>>;
};
Copy the code
Note that we use import() because we use ts. If we just use node syntax, we can just require it
index.ts
import Router from '@koa/router';
import _ from 'lodash';
import { Ictx, Iobj } from '.. /.. /typings/structure';
import { importModule } from './utils';
import Koa from 'koa';
const route = async (koa: Koa) => {
const router = new Router();
const list = await importModule();
for (const fileAll of list) {
{XXX: {url,methods,callback}
// Filter unqualified modules
for (const file of Object.values(fileAll)) {
if(! _.isObjectLike(file) || ! ['url'.'methods'.'callback'].every((f) = > Object.keys(file).includes(f))) {
continue;
}
const { url, methods, callback } = file;
const methodsArr = _.isArray(methods) ? methods : [methods];
for (const met of methodsArr) {
router[met](url, async (ctx, next) => {
constcontent: Ictx = { ... ctx,sendData: (data: Iobj, message = 'ok') = > {
ctx.body = {
data,
message,
code: 200}; ctx.type ='application/json';
},
throwError: (message = 'wrong', code = 400) = > {
ctx.body = {
code,
message,
};
ctx.type = 'application/json'; }};try {
await callback(content);
} catch (e) {
ctx.body = {
code: 400.message: (e instanceof Error ? e.message : e) || 'System error'}; ctx.status =400;
}
await next();
});
}
}
}
koa.use(router.routes()).use(router.allowedMethods());
};
export default route;
Copy the code
The log
Logging is also easy to implement with KOA’s middleware, using Winston as an example
The log is used to record errors while the system is running. If you remember the example above, let it continue to throw errors, and write the errors directly to the file through the middleware try.
import winston from 'winston';
import Koa from 'koa';
import 'winston-daily-rotate-file';
const transport = new winston.transports.DailyRotateFile({
filename: 'log/%DATE%.log'.datePattern: 'YYYY-MM-DD-HH'.zippedArchive: true.maxSize: '20m'.maxFiles: '14d'});const logger = winston.createLogger({
transports: [transport],
});
const asyncwinston = async (_ctx: Koa.ParameterizedContext<Koa.DefaultState, Koa.DefaultContext>, next: Koa.Next) => {
try {
await next();
} catch (err) {
const data = {
data: err,
time: new Date().valueOf(),
};
if (err instanceof Error) {
data.data = {
content: err.message,
name: err.name,
stack: err.stack,
};
}
logger.error(JSON.stringify(data)); }};export default asyncwinston;
Copy the code
Start the
Launching is simple, importing the index.js exposed above via koA’s use
App.js
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const route = require('./middleware/route');
const winston = require('./middleware/winston');
const App = async() = > {const app = new Koa();
app.use(winston);
app.use(bodyParser());
await route(app);
return app;
};
module.exports = App;
Copy the code
start.js
const Koa = require('koa');
const ip = require('ip');
const App = require('./App');
const start = async() = > {const app = await App();
notice(app);
};
const notice = (koa: Koa) = > {
const port = 3000;
const ipStr = ip.address();
const str = `http://${ipStr}:${port}`;
koa.listen(port, () = > {
console.log(The server runs on \n${str}`);
});
};
start();
Copy the code
Here’s a little explanation of why it’s split into two files, it’s because interface tests are deliberately layered,start
For startup purposes only
Finally, add a node-dev module and you’re done
10/21 added
The above node-dev is used in the development environment to facilitate quick restart of the code. Pm2 can be used in the production environment
/ / installation
yarn add node-dev
/ / start
node-dev start.js
Copy the code
The main features of node-dev startup are the ease of modifying the interface, the direct reloading of the interface, and the more obvious way of notification
The interface test
Catch up on 12/21
Create an index.ts file in the SRC directory to test the interface
import { Istructure } from '.. /typings/structure';
const testGet: Istructure = {
url: '/api/:id'.methods: 'get'.async callback(ctx) {
const{ id } = ctx? .params; ctx? .sendData({name: 'hello', id }); }};const testPost: Istructure = {
url: '/api'.methods: 'post'.async callback(ctx) {
const body = ctx?.request.body;
ctx?.sendData(body || {});
},
};
export { testGet, testPost };
Copy the code
After a simple POST and GET request, we create a __test__ directory and create an index.test.js file in it
yarn add @babel/core @babel/preset-env @babel/preset-typescript babel-jest jest supertest
Copy the code
Create a new babel.config.js file in the root directory
// babel.config.js
module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current'}}].'@babel/preset-typescript']};Copy the code
In short, Babel allows us to use es6 Module syntax in JS and convert TS files to JS, otherwise our test case would not run at all. In terms of the testing framework, JEST testing HTTP library and Supertest are selected. In fact, this part can be adjusted. The purpose of unit test is to compare whether the data meets expectations
__test__ index.test.js
import App from '.. /App';
import supertest from 'supertest';
test('GET request test'.async() = > {const app = await App();
const request = supertest(app.listen());
const id = 6;
const data = { name: 'hello'.id: `${id}` };
const res = await request.get(`/api/${id}`).expect(200);
const body = res.body.data;
expect(body).toEqual(data);
});
test('Post request Test'.async() = > {const app = await App();
const request = supertest(app.listen());
const data = { name: 'hello'.id: 8 };
const res = await request.post(`/api/`).send(data).expect(200);
const body = res.body.data;
expect(body).toEqual(data);
});
Copy the code
If we run NPX jest and the command line does not throw an exception, our code is as expected. See the documentation for more information on Jest
The last
The source code is placed in the repository
If it is helpful to you, please welcome stat. If there is any mistake, please point out that I wanted to use TS as an example for all the code, but TS is not necessary. So I just changed some scenes by hand