This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.

1. The background

Network IO verification layer is necessary

Suppose we want to develop a service that does the following.

  1. Request and participate:{list: [' xiao Ming ', 'Zhang SAN ']}
  2. Store the incoming list into the database
  3. The response:{MSG: 'success '}

For service stability, we need pre-validation when the user passes a list that is not an array or the elements of a list that are not strings, for example:

if (Array.isArray(req.list)
    && req.list.every(item= >
        typeof item === 'string')) {
    // The input is valid
}
else {
    // Input is abnormal
}
Copy the code

However, the interface input that is actually developed is usually more complex than the example above. Layering prevents normal processing logic from being mixed up with type validation and exception handling.

The NETWORK I/O verification layer verifies input and output by protocol

agreement

The validation layer requires us to provide expectations for IO, i.e., protocols. For the previous service, our expectations are as follows:

// Text description- request: list mandatory, which is a string. - Response: MSG Mandatory, which is a string// typescript type declarations
interface Request {
    list: string[];
}
interface Response {
    msg: string;
}

// koa-joi-router
{
    validate: {
        type: 'json'.body: {
            list: Joi.array().items(Joi.string().required()).required(),
        },
        output: {
            200: {
                body: {
                    message: Joi.string().required()
                }
            }
        }
    }
}
Copy the code

For ts implemented services, the network IO verification layer + protocol design has additional benefits.

IO validation in front compensates for ts’s inability to run type checking.

2. Koa – joi – the introduction of the router

As the name indicates, koA-Joi-router is a combination of JOI and KOA-Router. It is a route based on THE I/O verification capability of KOA. Among them, JOI focuses on data structure validation of JS.

Example 3.

npm i koa-joi-router -D
Copy the code
const koa = require('koa');
const router = require('koa-joi-router');
const Joi = router.Joi;

const public = router();

public.route({
  method: 'post'.path: '/signup'.validate: {
    body: {
      name: Joi.string().max(100),
      email: Joi.string().lowercase().email(),
      password: Joi.string().max(100),
      _csrf: Joi.string().token()
    },
    type: 'form'.output: {
      200: {
        body: {
          userId: Joi.string(),
          name: Joi.string()
        }
      }
    }
  },
  handler: async (ctx) => {
    const user = await createUser(ctx.request.body);
    ctx.status = 201; ctx.body = user; }});const app = new koa();
app.use(public.middleware());
app.listen(3000);
Copy the code

There are several ways to write new routes

// case 1
public.route({
  method: 'post'.path: '/signup'.validate: {
    body: {
      name: Joi.string().max(100),
      email: Joi.string().lowercase().email(),
      password: Joi.string().max(100),
      _csrf: Joi.string().token()
    },
    type: 'form'.output: {
      200: {
        body: {
          userId: Joi.string(),
          name: Joi.string()
        }
      }
    }
  },
  handler: async (ctx) => {
    const user = await createUser(ctx.request.body);
    ctx.status = 201; ctx.body = user; }});// case 2
public.post('/signup', {
  validate: {
    body: {
      name: Joi.string().max(100),
      email: Joi.string().lowercase().email(),
      password: Joi.string().max(100),
      _csrf: Joi.string().token()
    },
    type: 'form'.output: {
      200: {
        body: {
          userId: Joi.string(),
          name: Joi.string()
        }
      }
    }
  },
  handler: async (ctx) => {
    const user = await createUser(ctx.request.body);
    ctx.status = 201; ctx.body = user; }});// case 3
public.post('/signup', {
  validate: {
    body: {
      name: Joi.string().max(100),
      email: Joi.string().lowercase().email(),
      password: Joi.string().max(100),
      _csrf: Joi.string().token()
    },
    type: 'form'.output: {
      200: {
        body: {
          userId: Joi.string(),
          name: Joi.string()
        }
      }
    }
  }
}, async (ctx) => {
  const user = await createUser(ctx.request.body);
  ctx.status = 201;
  ctx.body = user;
});

Copy the code

Typescript support

Just introduce the corresponding type dependency.

npm i @types/koa-joi-router -D
Copy the code
// ts.config.json
{
    // ...
    "compilerOptions": {
        // ...
        "types": [
            // ...
            "koa-joi-router",].}}Copy the code

Directory structure division

In practice, we divide the directory structure based on our understanding of koa-Joi-Router.

API | _ the run / / actual business module, named after the corresponding path | processing | _ _ handler / / normal business index / / protocol description, exception handling | _ schema / / ts attention, The type declaration of Req and Res is main // service start, each middleware mount router // route write, and call each service module in APICopy the code

3. Exception handling

public.post('/signup', {
  validate: {
    type: 'json'.body: {
      list: Joi.array().items(Joi.string().required()).required(),
    },
    output: {
      200: {
        body: {
          message: Joi.string().required()
        }
      }
    }
  },
  async handler(ctx) {
    // Normal business processing}});Copy the code

For example, if the request body is {} and the verification fails, the response “list” is required. {“message”: “\”list\” is required”}

public.post('/signup', {
  validate: {
    type: 'json'.body: {
      list: Joi.array().items(Joi.string().required()).required(),
    },
    output: {
      200: {
        body: {
          message: Joi.string().required()
        }
      }
    },
    continueOnError: true // The handler is still executed
  },
  async handler(ctx) {
    if (ctx.invalid) {
      ctx.body = {message: ctx.invalid? .body? .msg};// Wrap the error as a valid output structure
    } else {
      // Normal business processing}}});Copy the code

ContinueOnError enables handler handler functions to be executed even when exceptions occur. Wrap CTx. invalid as a valid output structure in handler.

4. Protocol description

The protocol description uses chained calls. See the JOI documentation for details, listing some common apis.

The name of the describe
any.failover Failed to validate alternative values
any.default Check withundefinedIs the default value of
any.required The value is not acceptedundefined.All constructs are accepted by defaultundefined
any.required The value is not acceptedundefined.All constructs are accepted by defaultundefined
any.allow Some values that need to be exempted,Like the empty string of string
any.valid Restrict types to specified literals
object.unknown Accept other keys that are not verified,Extra keys are not accepted by default
string.regex Verifies whether a regular is matched

Ps: Each type corresponds to the joi factory function. For example, string corresponds to joi.string (), and any refers to all factory functions.

5. Verify the relationship with TS assertions

The TS and validation layers come into play at compile and run time, respectively, and therefore cannot be correlated automatically. We can assume that what is going into the business logic through validation is what we expect, giving assertions.

interface Req {

}

if (ctx.invalid) {
    // Check exception
}
else {
    try {
        ctx.body = await main(ctx.request.body as Req); // Type assertion
    } catch (err) {
        // The internal operation is abnormal}}Copy the code

You need to manually ensure that the verification protocol can meet the TS assertion, so as to avoid the “fish” that passes the verification and is not processed. For example, the following two are equivalent:

// joi
{
    message: Joi.string().required().allow(' ')}// ts
interface Req {
    message: string
}
Copy the code

Joi’s API is far richer than TS’s type system. It is also possible for the protocol to be stricter than the TS assertion, for example:

// joi
{
    email: Joi.string().required().email()
}

// ts
interface Req {
    email: string
}
Copy the code

The above situation requires the premise that the same standard is used for the front and back end verification.

Imagine a form that passes front-end validation, but is submitted with an interface error “input exception”. There are two general ideas:

  • The front and back ends are implemented by JS and maintain a one-to-one relationship, which can rely on JOI verification.
  • The front and back ends are decoupled, and the key data structures are verified by the back end.

Koa-joi-router is just one option to remedy the TS runtime problem. From the integration perspective of the framework, koA-Joi-Router is the right choice for KOA.