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 tokoaLet’s talk about it herekoaMiddleware, the following code is often used,koaUsing 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

  • usegetTo obtain resources
  • usepostTo send the request
  • useputTo update resources
  • usedeleteTo 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,startFor 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