As we all know, KOA2 is a very lightweight server framework based on NodeJS, and its easy to use features greatly save the front-end staff to develop the server API cost. Although many functions can be implemented, as a literate developer, the level of code and maintainability need to be considered.

To be honest, following the official KOA documentation, our code is not pretty.

Here we need to have a very clear understanding before we start coding: how is our code organized? How to layer? How to reuse?

After a series of thinking and some project practice, I summed up some koA development skills, can greatly improve the quality of the project code, no longer let the partner laugh at the bad code!

I. Automatic loading of routes

We used to register our routes manually, right? It goes something like this:

//app.js
const Koa = require('koa');
const app = new Koa(); 

const user = require('./app/api/user');
const store = require('./app/api/store');

app.use(user.routes());
app.use(classic.routes());
Copy the code

Is this code familiar to anyone who has written a KOA project? In fact, there are only two routing files, but in fact the number of such files is so large that it would be tedious to introduce reuse mode like this. Is there a way to automatically import and use these files?

There is. Now let’s install a very useful package:

npm install require-directory --save
Copy the code

Now just do this:

/ /...
const Router = require('koa-router'); 
const requireDirectory = require('require-directory');
//module is a fixed argument, './ API 'is the path to the routing file (supports files in nested directories), and visit in the third argument is a callback function
const modules = requireDirectory(module.'./app/api', {
    visit: whenLoadModule
});
function whenLoadModule(obj) {
    if(obj instanceofRouter) { app.use(obj.routes()); }}Copy the code

So, good code can improve efficiency, this auto-load route saves a lot of registration configuration effort, isn’t it cool?

Two, use the manager to extract the contents of the entry file

I believe many people do this: the route registration code is written in the entry file app.js, and the subsequent import of the corresponding middleware is also written in this file. But for the entry file, we don’t want it to be too bloated, so we can take some of the operations out of it.

Create a folder called core in your root directory, where you’ll store all of your public code.

//core/init.js
const requireDirectory = require('require-directory');
const Router = require('koa-router'); 

class InitManager {
    static initCore(app) {
        // Pass in the koA instance from app.js
        InitManager.app = app;
        InitManager.initLoadRouters();
    }
    static initLoadRouters() {
        // Note that the path is dependent on the current file location
        // It is best to write an absolute path
        const apiDirectory = `${process.cwd()}/app/api`
        const modules = requireDirectory(module, apiDirectory, {
            visit: whenLoadModule
        });
        function whenLoadModule(obj) {
            if(obj instanceof Router) {
                InitManager.app.use(obj.routes())
            }
        }
    }
}

module.exports = InitManager;
Copy the code

It’s now in app.js

const Koa = require('koa');
const app = new Koa();

const InitManager = require('./core/init');
InitManager.initCore(app);
Copy the code

We can say that it has been streamlined a lot, and the implementation of the functionality is still no problem.

Three, development environment and production environment distinction

Sometimes, we need to do different processing in two different environments, and we need to inject parameters in the global in advance.

First, in the project root directory, create the config folder:

//config/config.js
module.exports = {
  environment: 'dev'
}
Copy the code
// Add the following contents to the initManager class of core/init.js
static loadConfig() {
    const configPath = process.cwd() + '/config/config.js';
    const config = require(configPath);
    global.config = config;
}
Copy the code

The current environment can now be retrieved from the global variable.

Global exception handling middleware

1. Pit for asynchronous exception handling

Exception handling is a very important part of the server-side API writing process, because not every function returns the result we want. Whether it’s a syntax error or a business logic error, exceptions need to be thrown so that the problem is exposed in the most intuitive way, rather than being ignored. When a function encounters an exception, the best way to do so is not to return false/null, but to have the exception thrown directly.

In JS, a lot of times we’re writing asynchronous code, timers, promises, etc., and this creates a problem. We can’t catch errors in asynchronous code if we use try/catch. Such as:

function func1() {
  try {
    func2();
  } catch (error) {
    console.log('error'); }}function func2() {
  setTimeout((a)= > {
    throw new Error('error')},1000)
}

func1();
Copy the code

Execute this code and you’ll see that after a second the console. Log (‘error’) did not execute, i.e. func1 did not catch the exception of Func2. This is the problem with asynchrony.

So how do we fix this hole?

The easiest way is to take async-await.

async function func1() {
  try {
    await func2();
  } catch (error) {
    console.log('error'); }}function func2() {
  return new Promise((resolve, reject) = > {
    setTimeout((a)= > {
      reject()
    }, 1000)
  })
}

func1();
Copy the code

The asynchronous function here is encapsulated by a Promise, and then reject triggers a catch in func1, which catches the exception in Func2. Fortunately for asynchronous code like Func2, the usual libraries (axios, Sequelize) have wrapped Promise objects for us, so we don’t have to wrap them ourselves, just try/catch them with async-await.

Note: this way you must await any asynchronous code before executing it and report an Unhandled promise rejection error. The lesson of blood!

2. Design exception handling middleware

//middlewares/exception.js
// The job here is to capture the interface returned by the exception generation
const catchError = async (ctx, next) => {
  try {
    await next();
  } catch (error) {
    if(error.errorCode) {
      ctx.body = {
        msg: error.msg,
        error_code: error.errorCode,
        request: `${ctx.method} ${ctx.path}`
      };
    } else {
      // Special handling is used for unknown exceptions
      ctx.body = {
        msg: 'we made a mistake'}; }}}module.exports = catchError;
Copy the code

To the entry file using this middleware.

//app.js
const catchError = require('./middlewares/exception');
app.use(catchError)
Copy the code

Next, let’s use HttpException as an example to generate a specific type of exception.

//core/http-exception.js
class HttpException extends Error {
  // MSG indicates an exception message, errorCode indicates an errorCode (an internal developer convention), and code indicates the HTTP status code
  constructor(MSG =' server exception ', errorCode=10000, code=400) {super(a)this.errorCode = errorCode
    this.code = code
    this.msg = msg
  }
}

module.exports = {
  HttpException
}
Copy the code
//app/api/user.js
const Router = require('koa-router')
const router = new Router()
const { HttpException } = require('.. /.. /core/http-exception')

router.post('/user', (ctx, next) => {
    if(true) {const error = new HttpException('Network request error'.10001.400)
        throw error
  }
})
module.exports = router;
Copy the code

The interface returned looks like this:

This throws a specific type of error. But the types of errors in business are very complex, so I’m going to share some Exception classes that I wrote for your reference:

//http-exception.js
class HttpException extends Error {
  constructor(MSG = 'server exception ', errorCode=10000, code=400) {super(a)this.error_code = errorCode
    this.code = code
    this.msg = msg
  }
}

class ParameterException extends HttpException{
  constructor(msg, errorCode){
    super(400, msg='Parameter error', errorCode=10000); }}class NotFound extends HttpException{
  constructor(msg, errorCode) {
    super(404, msg='Resource not found', errorCode=10001); }}class AuthFailed extends HttpException{
  constructor(msg, errorCode) {
    super(404, msg='Authorization failed', errorCode=10002); }}class Forbidden extends HttpException{
  constructor(msg, errorCode) {
    super(404, msg='Access denied', errorCode=10003);
    this.msg = msg || 'Access denied';
    this.errorCode = errorCode || 10003;
    this.code = 404; }}module.exports = {
  HttpException,
  ParameterException,
  Success,
  NotFound,
  AuthFailed,
  Forbidden
}
Copy the code

For error-handling code that needs to be called frequently, it makes sense to place it globally instead of importing it every time.

Now init.js looks like this:

const requireDirectory = require('require-directory');
const Router = require('koa-router');

class InitManager {
  static initCore(app) {
    // The entry method
    InitManager.app = app;
    InitManager.initLoadRouters();
    InitManager.loadConfig();
    InitManager.loadHttpException();// Add global Exception
  }
  static initLoadRouters() {
    // path config
    const apiDirectory = `${process.cwd()}/app/api/v1`;
    requireDirectory(module, apiDirectory, {
      visit: whenLoadModule
    });

    function whenLoadModule(obj) {
      if (obj instanceofRouter) { InitManager.app.use(obj.routes()); }}}static loadConfig(path = ' ') {
    const configPath = path || process.cwd() + '/config/config.js';
    const config = require(configPath);
    global.config = config;
  }
  static loadHttpException() {
    const errors = require('./http-exception'); global.errs = errors; }}module.exports = InitManager;
Copy the code

5. Use JWT to complete authentication and authorization

JWT(Json Web Token) is one of the most popular cross-domain authentication solutions today. It works like this:

1. The front end transfers the user name and password to the back end

2. After the user name and password are verified successfully at the backend, a token(or a token stored in a cookie) is returned to the front-end.

3. The front-end takes the token and saves it

4. Token authentication is performed before the front-end accesses the back-end interface.

So what do we need to do in KOA?

In the token generation stage: the account is verified first, then the token is generated and passed to the front end.

In the token authentication stage: the authentication middleware is written and the front-end access is intercepted. After token authentication, the back-end interface can be accessed.

1. The token is generated

Install two packages first:

npm install jsonwebtoken basic-auth --save
Copy the code
//config.js
module.exports = {
  environment: 'dev'.database: {
    dbName: 'island'.host: 'localhost'.port: 3306.user: 'root'.password: 'fjfj'
  },
  security: {
    secretKey: 'lajsdflsdjfljsdljfls'.// The key used to generate the token
    expiresIn: 60 * 60// Expiration time}}//utils.js 
// Generate token function, uid is user ID, scope is permission level (type is number, internal convention)
const generateToken = function(uid, scope){
    const { secretKey, expiresIn } = global.config.security
    // The first parameter is the js object with user information, the second parameter is the key used to generate the token, and the third parameter is the configuration item
    const token = jwt.sign({
        uid,
        scope
    },secretKey,{
        expiresIn
    })
    return token
}
Copy the code

2.Auth middleware interception

// Add Authorization: 'Basic to the request header${base64(token+":")}//middlewares/auth.js const basicAuth = require()'basic-auth');
const jwt = require('jsonwebtoken'); class Auth { constructor(level) { Auth.USER = 8; Auth.ADMIN = 16; this.level = level || 1; } // Notice that m is a property getm() {
    return async (ctx, next) => {
      const userToken = basicAuth(ctx.req);
      let errMsg = 'Token is not legal';

      if(! userToken || ! userToken.name) { throw new global.errs.Forbidden(); } try {// Verify the token value passed from the front end. If successful, a decode object will be returned. Contains the uid and scope var decode = JWT. Verify (userToken. Name, global. Config. Security. SecretKey); } Catch (error) {// Token is invalid // or token expires // throw an exception errMsg ='// as the case may be 'throw new global.errs.Forbidden(errMsg); Auth = {uid: decode. Uid, scope: decode. Scope}; // Now walk here token authentication through await next(); } } } module.exports = Auth;Copy the code

Write the following in the corresponding routing file:

// Middleware takes precedence. If authentication fails in the middleware, it does not go to the routing logic
router.post('/xxx'.new Auth().m , async (ctx, next) => {
    / /...
})
Copy the code

Require path alias

Over the course of development, as the project directory becomes more complex, the package reference path becomes more cumbersome. There has been an import path like this:

const Favor = require('.. /.. /.. /models/favor');
Copy the code

There are even more lengthy import methods than this, as a code cleanliness fetish programmer, it is very unpleasant to see. The absolute path process.cwd() can also solve this problem, but when the directory is too deep, the imported code can be very cumbersome. Is there a better solution?

Alias the path using module-alias.

npm install module-alias --save
Copy the code
//package.json add the following
  "_moduleAliases": {
    "@models": "app/models"
  },
Copy the code

Then import the library in app.js:

// select * from *
require('module-alias/register');
Copy the code

Now the imported code looks like this:

const Favor = require('@models/favor');
Copy the code

It’s much cleaner and easier to maintain.

Use sequelize transactions to resolve data inconsistencies

When a business for a number of database operations, with thumb up function, for example, the first thing you have to record the increase in the table of the thumb up, then you have to add the thumb up several of the corresponding object 1, this is two operations must be done together, if you have a successful operation, another operating problems, it will lead to inconsistent data, it is a very serious security problem.

We want to roll back to the pre-operation state if anything goes wrong. Database transaction operations are recommended at this point. This can be done using sequelize transactions, and the code for the business section is posted below:

async like(art_id, uid) {
    // Check if there are any duplicates
    const favor = await Favor.findOne({
      where: { art_id, uid }
      }
    );
    // If there is a repeat, throw an exception
    if (favor) {
      throw new global.errs.LikeError('You already liked it.');
    }
    // DB is the sequelize instance
    // Here are the transaction operations
    return db.transaction(async t => {
      //1. Create a thumbs-up record
      await Favor.create({ art_id, uid }, { transaction: t });
      //2
      const art = await Art.getData(art_id, type);// Get the likes
      await art.increment('fav_nums', { by: 1.transaction: t });// Add 1
    });
  }
Copy the code

Transaction in Sequelize does something like this. The official document is promise and looks pretty ugly. It would be much better to change async/await, but don’t forget to write await.

The code optimization for KOA2 is shared here and will be continued in the future. Welcome to like and leave a comment!