This article is written by mRc, a member of the Tuquai community. Welcome to join tuquai community and create wonderful free technical tutorials together to help the development of the programming industry.

If you think we wrote well, remember to like + follow + comment three times, encourage us to write a better tutorial 💪

With its elegant “Onion model” and full support for Promise and async/await asynchronous programming, the Koa framework has captivated Node enthusiasts since its inception. However, Koa itself is just a simple middleware framework, and there are many surrounding ecosystems to support a sufficiently complex Web application. This tutorial will not only walk you through the basics of Koa, but also take a good look at the components (routing, database, authentication, etc.) necessary to build a Web application to achieve a complete user system.

start

Koa, as the new generation of Node.js Web framework created by the original team of Express, has attracted much attention since its release. As the Koa authors point out in the document:

The Philosophically, Koa aims to “fix and replace nodes “, whereas Express “augments nodes “. Koa is meant to solve Node problems and replace them.)

In this article, we will walk you through the development of a simple USER system REST API that supports user add, remove, change, and query, as well as JWT authentication, and get a taste of the essence of Koa2, which is a breakthrough change from Express. We will choose TypeScript as the development language, MySQL as the database, and TypeORM as the database bridge layer.

Pay attention to

This article will not cover the principles of Koa source-level analysis, but will focus on giving you a complete grasp of how to use Koa and its surrounding ecology to develop Web applications and appreciate the beauty of Koa design. In addition, this tutorial is quite long, if a cup of tea is not enough, you can refill it

Preliminary knowledge

This tutorial assumes you already have the following knowledge:

  • JavaScript language basics (including some common ES6+ syntax)
  • Learn the basics of Node.js, as well as the basics of using NPM, in this tutorial
  • Learn the basics of TypeScript. Simple type annotations are all you need to know. Check out our TypeScript tutorial series
  • * (not required) * Basic knowledge of the Express framework will go a long way to experiencing the beauty of Koa, and there will be plenty of comparisons to Express throughout this article, as you can see in this tutorial

The technology used

  • Node.js: 10.x and above
  • NPM: 6.x and above
  • Koa: 2. X
  • MySQL: Stable version 5.7 or later is recommended
  • TypeORM: 0.2 x

Learning goals

By the end of this tutorial, you will have learned:

  • If you write Koa middleware
  • through@koa/routerImplementing route Configuration
  • Connect to and read from the MySQL database via TypeORM (all other databases are similar)
  • Understand the principle of JWT authentication and implement it
  • Master Koa error handling mechanisms

Prepare the initial code

We have the project scaffolding ready for you, run the following command to clone our initial code:

git clone -b start-point https://github.com/tuture-dev/koa-quickstart.git
Copy the code

If you have trouble accessing GitHub, clone our Gitee repository:

git clone -b start-point https://gitee.com/tuture/koa-quickstart.git
Copy the code

Then enter the project and install the dependencies:

cd koa-quickstart && npm install
Copy the code

Pay attention to

Here I used package-lock.json to ensure that all dependencies are the same. If you have problems installing dependencies using YARN, it is recommended to remove node_modules and reinstall NPM install.

The simplest Koa server

Create SRC /server.ts and write your first Koa server as follows:

// src/server.ts
import Koa from 'koa';
import cors from '@koa/cors';
import bodyParser from 'koa-bodyparser';

// Initialize the Koa application instance
const app = new Koa();

// Register middleware
app.use(cors());
app.use(bodyParser());

// Respond to user requests
app.use((ctx) = > {
  ctx.body = 'Hello Koa';
});

// Run the server
app.listen(3000);
Copy the code

The entire process is almost identical to a basic Express server:

  1. Example Initialize an application instanceapp
  2. Register associated middleware (across domainscorsAnd request body resolution middlewarebodyParser)
  3. Add request handlers to respond to user requests
  4. Running server

At first glance, the Request Handler in Step 3 looks different. In the Express framework, a request handler normally looks like this:

function handler(req, res) {
  res.send('Hello Express');
}
Copy the code

Two parameters correspond to the Request object (Request) and the Response object (Response), but in Koa, the Request handler has only one parameter CTX (Context), Then simply write the relevant attributes to the following object (in this case, to the returned data body) :

function handler(ctx) {
  ctx.body = 'Hello Koa';
}
Copy the code

Oh, my God, is Koa cutting corners on this? Don’t worry, we’ll see how Koa’s design is unique when we cover middleware in the next section.

Running server

We can start the server with NPM start. To test our API with Curl (or Postman, etc.) :

$ curl localhost:3000
Hello Koa
Copy the code

prompt

Nodemon has been configured in our scaffolding, so there is no need to shut down the server. After saving the modified code, the latest code will be automatically loaded and run.

The first Koa middleware

Strictly speaking, Koa is just a middleware framework, as its introduction states:

ES2017 async functions are used to write Expressive Node.js middleware. ES2017 async functions are used to write Expressive Node.js middleware.

This chart illustrates the contrast between Koa and Express:

As you can see, Koa is actually targeted at Connect (the middleware layer underneath Express) and does not include other features that Express has, such as routing, template engines, sending files, etc. Next, let’s look at the most important thing Koa knows: middleware.

The famous Onion model

You’ve probably never used the Koa framework, but you’ve probably heard of the Onion model, and Koa is one of the representative frameworks for the Onion model. Here’s a picture you might be familiar with:

However, in my opinion, this graph is too much like “onion” to be easy to understand. Let’s take a look at the beauty of Koa middleware design in a more clear and intuitive way. First let’s take a look at what Express middleware looks like:

Requests flow directly through the middleware, and a Response is returned via Request handlers. Then let’s look at what Koa’s middleware looks like:

As you can see, unlike Express middleware, Koa middleware does not complete its mission once the request is approved; Instead, middleware execution is clearly divided into two phases. Let’s take a look at what Koa middleware looks like in a moment.

Definition of Koa middleware

Koa’s middleware is a function like this:

async function middleware(ctx, next) {
  // Phase 1
  await next();
  // Phase 2
}
Copy the code

The first parameter is the Koa Context, which is passed by the green arrow that runs through all the middleware and request processing functions in the figure above, and encapsulates the request body and response body (there are actually other attributes, but I won’t go into them here). Ctx. request and ctx.response can be obtained respectively. Here are some common attributes:

ctx.url    // equivalent to ctx.request.url
ctx.body   // equivalent to ctx.response.body
ctx.status // equivalent to ctx.response.status
Copy the code

prompt

Refer to the Context API documentation for all attributes and nicknames on requests and responses.

The second argument to the middleware is the next function, which those of you familiar with Express will know what it does: it transfers control to the next middleware. However, the essential difference between it and Express’s Next function is that Koa’s Next function returns a Promise. After this Promise enters the Fulfilled state, the code of the second stage in middleware will be implemented.

So we have to ask: what is the benefit of splitting middleware execution into two phases? Let’s take a look at a very classic example: logging middleware (including response time calculations).

Combat: Logging middleware

Let’s implement a simple logging middleware logger that logs the method, URL, status code, and response time of each request. Create SRC /logger.ts as follows:

// src/logger.ts
import { Context } from 'koa';

export function logger() {
  return async (ctx: Context, next: (a)= > Promise<void>) = > {const start = Date.now();
    await next();
    const ms = Date.now() - start;
    console.log(`${ctx.method} ${ctx.url} ${ctx.status} - ${ms}ms`);
  };
}
Copy the code

Strictly speaking, logger is a middleware Factory function, and the result returned by calling this Factory function is the true Koa middleware. We write it as a factory function because we can better control the behavior of the middleware by passing parameters to the factory function (of course logger is simple and has no parameters).

In the first stage of this middleware, we get the incoming time of the request by date.now (), and then give execution right by await next(), and wait for the downstream middleware to finish running. In the second stage, we calculate the time to process the request by calculating the difference of date.now ().

If you use Express to implement this functionality, how should middleware be written? Would it be as simple and elegant as Koa?

prompt

Using the difference between date.now () to calculate the running time is not exact. To get a more accurate time, use process.hrtime().

SRC /server.ts registers the logger middleware through app.use, and the code is as follows:

// src/server.ts
// ...

import { logger } from './logger';

// Initialize the Koa application instance
const app = new Koa();

// Register middleware
app.use(logger());
app.use(cors());
app.use(bodyParser());

// ...
Copy the code

Using Curl or some other request tool, you should see the following output:

The Koa framework itself is pretty much covered, but for a more complete Web server, we need more “armaments” to deal with the day-to-day business logic. In the next section, we’ll address two key issues with the community’s excellent components: routing and databases, and demonstrate how to use them in conjunction with the Koa framework.

Implementing route Configuration

Since Koa is only a middleware framework, the implementation of routing requires a separate NPM package. Start by installing @koa/ Router and its TypeScript type definition:

$ npm install @koa/router
$ npm install @types/koa__router -D
Copy the code

Pay attention to

Some tutorials use the Koa-Router, but since the Koa-Router is currently in a nearly unmaintained state, we’ll use the more aggressively maintained Fork version @koa/ Router here.

Routing planning

In this tutorial, we will implement the following routes:

  • GET /users: Queries all users
  • GET /users/:id: Queries a single user
  • PUT /users/:id: Updates a single user
  • DELETE /users/:id: Deletes a user
  • POST /users/login: Login (obtain JWT Token)
  • POST /users/register: Registered user

To implement the Controller

Create the controllers directory in SRC to hold the controller-related code. AuthController: SRC /controllers/auth.ts

// src/controllers/auth.ts
import { Context } from 'koa';

export default class AuthController {
  public static async login(ctx: Context) {
    ctx.body = 'Login controller';
  }

  public static async register(ctx: Context) {
    ctx.body = 'Register controller'; }}Copy the code

Then create SRC /controllers/user.ts as follows:

// src/controllers/user.ts
import { Context } from 'koa';

export default class UserController {
  public static async listUsers(ctx: Context) {
    ctx.body = 'ListUsers controller';
  }

  public static async showUserDetail(ctx: Context) {
    ctx.body = `ShowUserDetail controller with ID = ${ctx.params.id}`;
  }

  public static async updateUser(ctx: Context) {
    ctx.body = `UpdateUser controller with ID = ${ctx.params.id}`;
  }

  public static async deleteUser(ctx: Context) {
    ctx.body = `DeleteUser controller with ID = ${ctx.params.id}`; }}Copy the code

Notice that in the next three controllers, we get the route parameter ID through ctx.params.

To implement the routing

Then we create SRC /routes.ts to mount the controller to the corresponding route:

// src/routes.ts
import Router from '@koa/router';

import AuthController from './controllers/auth';
import UserController from './controllers/user';

const router = new Router();

// Auth-related routes
router.post('/auth/login', AuthController.login);
router.post('/auth/register', AuthController.register);

// Routes associated with users
router.get('/users', UserController.listUsers);
router.get('/users/:id', UserController.showUserDetail);
router.put('/users/:id', UserController.updateUser);
router.delete('/users/:id', UserController.deleteUser);

export default router;
Copy the code

You can see that @koa/ Router is basically used in the same way as Express Router.

Registered routing

Finally, we need to register the router as middleware. Open SRC /server.ts and modify the code as follows:

// src/server.ts
// ...

import router from './routes';
import { logger } from './logger';

// Initialize the Koa application instance
const app = new Koa();

// Register middleware
app.use(logger());
app.use(cors());
app.use(bodyParser());

// Respond to user requests
app.use(router.routes()).use(router.allowedMethods());

// Run the server
app.listen(3000);
Copy the code

As can be seen, we call the routes method of the router object to obtain the corresponding Koa middleware, and call the allowedMethods method to register the MIDDLEWARE detected by THE HTTP method, so that when the user accesses the API through the incorrect HTTP method, The 405 Method Not Allowed status code is automatically returned.

We use Curl to test the route (you can also use Postman yourself) :

$ curl localhost:3000/hello Not Found $ curl localhost:3000/auth/register Method Not Allowed $ curl -X POST localhost:3000/auth/register Register controller $ curl -X POST localhost:3000/auth/login Login controller $ curl localhost:3000/users ListUsers controller $ curl localhost:3000/users/123 ShowUserDetail controller with ID = 123 $ curl  -X PUT localhost:3000/users/123 UpdateUser controller with ID = 123 $ curl -X DELETE localhost:3000/users/123 DeleteUser controller with ID = 123Copy the code

The server output logs are as follows:

Now that the route is connected, let’s access the real data.

Connect to the MySQL Database

From this step, we will officially access the database. Koa itself is a middleware framework that can theoretically plug into any type of database, and here we’ll choose the popular relational database MySQL. And because we are developing in TypeScript, TypeORM, an ORM library tailored for TS, is used here.

Database preparation

Install and configure the MySQL database in two ways:

  • Download the installation package from the official website. Here is the download address
  • Use the MySQL Docker image

After ensuring that the MySQL instance is running, we open the terminal and connect to the database from the command line:

$ mysql -u root -p
Copy the code

After entering the pre-set root password, enter the interactive execution client of MySQL and run the following command:

-- Create a database
CREATE DATABASE koa;

-- Create a user and grant permissions
CREATE USER 'user'@'localhost' IDENTIFIED BY 'pass';
GRANT ALL PRIVILEGES ON koa.* TO 'user'@'localhost';

-- Troubleshoot the authentication protocol problem of MySQL 8.0
ALTER USER 'user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'pass';
flush privileges;
Copy the code

TypeORM configuration and connection

Install NPM packages: MySQL driver, TypeORM, reflect-Metadata (reflection API library for TypeORM inferred model metadata)

$ npm install mysql typeorm reflect-metadata
Copy the code

Then create ormconfig.json in the project root directory, and TypeORM reads the database configuration to connect as follows:

// ormconfig.json
{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "user",
  "password": "pass",
  "database": "koa",
  "synchronize": true,
  "entities": ["src/entity/*.ts"],
  "cli": {
    "entitiesDir": "src/entity"
  }
}
Copy the code

Here are some fields that need to be explained:

  • databaseThat’s what we just createdkoaThe database
  • synchronizeSet totrueIt allows us to automatically synchronize the model definition to the database every time we change it * (if you have worked with other ORM libraries, this is automatic data migration) *
  • entitiesThe fields define the path to the model file, which we will create in a moment

SRC /server.ts; SRC /server.ts;

// src/server.ts
import Koa from 'koa';
import cors from '@koa/cors';
import bodyParser from 'koa-bodyparser';
import { createConnection } from 'typeorm';
import 'reflect-metadata';

import router from './routes';
import { logger } from './logger';

createConnection()
  .then((a)= > {
    // Initialize the Koa application instance
    const app = new Koa();

    // Register middleware
    app.use(logger());
    app.use(cors());
    app.use(bodyParser());

    // Respond to user requests
    app.use(router.routes()).use(router.allowedMethods());

    // Run the server
    app.listen(3000);
  })
  .catch((err: string) = > console.log('TypeORM connection error:', err));
Copy the code

Create data model definitions

Create an Entity directory under the SRC directory to hold the data model definition files. Create user.ts, representing the user model, as follows:

// src/entity/user.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity(a)export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column({ select: false })
  password: string;

  @Column()
  email: string;
}
Copy the code

As you can see, the user model has four fields whose meanings are easy to understand. TypeORM uses decorators to map our User class to tables in the database in an elegant way. Here we use three decorators:

  • EntityUse to decorate the entire class into a database model
  • ColumnDecorates a property of a class that corresponds to a column in a database table and can provide a series of option arguments, such as the one we givepasswordSet upselect: falseSo that this field is not selected by default when querying
  • PrimaryGeneratedColumnIs the decorator main column, whose value is automatically generated

prompt

Refer to the TypeORM decorator documentation for all of its decorator definitions and detailed usage.

Operate the database in the Controller

Then you can add, delete, change and check data in the Controller. SRC /controllers/user.ts (controllers/ controllers/user.ts)

// src/controllers/user.ts
import { Context } from 'koa';
import { getManager } from 'typeorm';

import { User } from '.. /entity/user';

export default class UserController {
  public static async listUsers(ctx: Context) {
    const userRepository = getManager().getRepository(User);
    const users = await userRepository.find();

    ctx.status = 200;
    ctx.body = users;
  }

  public static async showUserDetail(ctx: Context) {
    const userRepository = getManager().getRepository(User);
    const user = await userRepository.findOne(+ctx.params.id);

    if (user) {
      ctx.status = 200;
      ctx.body = user;
    } else {
      ctx.status = 404; }}public static async updateUser(ctx: Context) {
    const userRepository = getManager().getRepository(User);
    await userRepository.update(+ctx.params.id, ctx.request.body);
    const updatedUser = await userRepository.findOne(+ctx.params.id);

    if (updatedUser) {
      ctx.status = 200;
      ctx.body = updatedUser;
    } else {
      ctx.status = 404; }}public static async deleteUser(ctx: Context) {
    const userRepository = getManager().getRepository(User);
    await userRepository.delete(+ctx.params.id);

    ctx.status = 204; }}Copy the code

In TypeORM, manipulating the data Model is done primarily through Repository. In Controller, it is accessed via getManager().getrepository (Model). Repository’s query API will then be similar to that of other libraries.

prompt

Refer to the documentation here for all of Repository’s query apis.

If you’re careful, you’ll also notice that we got the request body data from ctx.request.body, which we added to the Context object with the bodyParser middleware configured in the first step.

Then we modify the AuthController to implement the specific registration logic. Since passwords cannot be stored in the database in plain text, asymmetric algorithms are needed for encryption. Here, we use the Argon2 algorithm, which has won the password encryption contest. Install the corresponding NPM package:

npm install argon2
Copy the code

SRC /controllers/auth.ts ()

// src/controllers/auth.ts
import { Context } from 'koa';
import * as argon2 from 'argon2';
import { getManager } from 'typeorm';

import { User } from '.. /entity/user';

export default class AuthController {
  // ...

  public static async register(ctx: Context) {
    const userRepository = getManager().getRepository(User);

    const newUser = new User();
    newUser.name = ctx.request.body.name;
    newUser.email = ctx.request.body.email;
    newUser.password = await argon2.hash(ctx.request.body.password);

    // Save to database
    const user = await userRepository.save(newUser);

    ctx.status = 201; ctx.body = user; }}Copy the code

Once the server is up and running, we can start testing the wave. The first is the registered user (I’m using Postman here to make it more intuitive) :

You can continue to register a few users, then continue to access /users related routes, should be able to successfully get, modify and delete the corresponding data!

Implement JWT authentication

JSON Web Token (JWT) is a popular RESTful API authentication scheme. Here we walk you through how to use JWT authentication in the Koa framework, but we won’t go into too much detail about how it works (see this article to learn more).

First install the relevant NPM package:

npm install koa-jwt jsonwebtoken
npm install @types/jsonwebtoken -D
Copy the code

Create SRC /constants.ts to store JWT Secret constants as follows:

// src/constants.ts
export const JWT_SECRET = 'secret';
Copy the code

In real development, replace it with a sufficiently complex string, preferably injected as an environment variable.

Replanning routes

Some routes we want only logged-in users to have access to (protected routes), while others are accessible to all requests (unprotected routes). In Koa’s Onion model, we can implement this:

All requests have direct access to the unprotected route, but the protected route is placed behind the JWT middleware (or “inside” from the Perspective of the Onion model), so that requests that do not carry a JWT Token are returned without further delivery.

With that in mind, open the SRC /routes.ts routing file and modify the code as follows:

// src/routes.ts
import Router from '@koa/router';

import AuthController from './controllers/auth';
import UserController from './controllers/user';

const unprotectedRouter = new Router();

// Auth-related routes
unprotectedRouter.post('/auth/login', AuthController.login);
unprotectedRouter.post('/auth/register', AuthController.register);

const protectedRouter = new Router();

// Routes associated with users
protectedRouter.get('/users', UserController.listUsers);
protectedRouter.get('/users/:id', UserController.showUserDetail);
protectedRouter.put('/users/:id', UserController.updateUser);
protectedRouter.delete('/users/:id', UserController.deleteUser);

export { protectedRouter, unprotectedRouter };
Copy the code

Above, we implemented protectedRouter and unprotectedRouter respectively, corresponding to routes that need JWT middleware protection and routes that do not need protection respectively.

Register JWT middleware

The next step is to register the JWT middleware, and the route unprotectedRouter that does not need protection and the route protectedRouter that needs protection are registered before and after it respectively. Modify the server file SRC /server.ts as follows:

// src/server.ts
// ...
import jwt from 'koa-jwt';
import 'reflect-metadata';

import { protectedRouter, unprotectedRouter } from './routes';
import { logger } from './logger';
import { JWT_SECRET } from './constants';

createConnection()
  .then((a)= > {
    // ...

    // Access without JWT Token
    app.use(unprotectedRouter.routes()).use(unprotectedRouter.allowedMethods());

    // Register JWT middleware
    app.use(jwt({ secret: JWT_SECRET }).unless({ method: 'GET' }));

    // JWT Token is required for access
    app.use(protectedRouter.routes()).use(protectedRouter.allowedMethods());

    // ...
  })
  // ...
Copy the code

Corresponding to the design drawing of “Onion model” just now, is it very intuitive?

prompt

After the JWT middleware is registered, if the user requests a valid Token, the protectedRouter can obtain the content of the Token through ctx.state.user. For example, ID); On the other hand, if the Token is missing or invalid, the JWT middleware automatically returns a 401 error directly. For more details on the use of KOA-JWT, please refer to its documentation.

Sign a JWT Token in Login

We need to provide an API port so that users can obtain JWT tokens, the most suitable of course is the login interface /auth/login. SRC /controllers/auth.ts; SRC /controllers/auth.ts;

// src/controllers/auth.ts
// ...
import jwt from 'jsonwebtoken';

// ...
import { JWT_SECRET } from '.. /constants';

export default class AuthController {
  public static async login(ctx: Context) {
    const userRepository = getManager().getRepository(User);

    const user = await userRepository
      .createQueryBuilder()
      .where({ name: ctx.request.body.name })
      .addSelect('User.password')
      .getOne();

    if(! user) { ctx.status =401;
      ctx.body = { message: 'Username does not exist' };
    } else if (await argon2.verify(user.password, ctx.request.body.password)) {
      ctx.status = 200;
      ctx.body = { token: jwt.sign({ id: user.id }, JWT_SECRET) };
    } else {
      ctx.status = 401;
      ctx.body = { message: 'Password error'}; }}// ...
}
Copy the code

In login, we first query the corresponding user based on the user name (the name field in the request body), and return 401 if the user does not exist. If yes, run argon2.verify to verify whether the plaintext password in the request body is the same as the encryption password stored in the database. If yes, jwt.sign is used to sign the Token. If no, return 401.

The Token payload is the user ID object {ID: user. ID}. In this way, the user ID can be obtained through ctx.user. ID after successful authentication.

Add access control to the User controller

After the middleware and issuance of the tokens are handled, the final step is to verify the user’s tokens in the appropriate place to verify that they have sufficient permissions. Typically, when updating or deleting a user, we want to make sure the user is doing it. SRC /controllers/user.ts

// src/controllers/user.ts
// ...

export default class UserController {
  // ...

  public static async updateUser(ctx: Context) {
    const userId = +ctx.params.id;

    if(userId ! == +ctx.state.user.id) { ctx.status =403;
      ctx.body = { message: 'No right to do this' };
      return;
    }

    const userRepository = getManager().getRepository(User);
    await userRepository.update(userId, ctx.request.body);
    const updatedUser = await userRepository.findOne(userId);

    // ...
  }

  public static async deleteUser(ctx: Context) {
    const userId = +ctx.params.id;

    if(userId ! == +ctx.state.user.id) { ctx.status =403;
      ctx.body = { message: 'No right to do this' };
      return;
    }

    const userRepository = getManager().getRepository(User);
    await userRepository.delete(userId);

    ctx.status = 204; }}Copy the code

The authentication logic of the two controllers is basically the same. We compare whether ctx.params.id and ctx.state.user.id are the same. If they are different, we return 403 Forbidden.

After writing the code, we access the login API with one of the user information we just registered:

Successfully obtained the JWT Token! We then copy the obtained Token and when testing the protected routes next we need to add an Authorization header with a value of Bearer

as shown below:

Then you can test the protected route! This is omitted for lack of space.

Error handling

Finally, let’s talk briefly about error handling in Koa. Because Koa uses async functions and promises as a solution for asynchronous programming, error handling is easy to do with JavaScript’s native try-catch syntax.

Implementing custom errors (exceptions)

First, let’s implement some custom error (or exception, not distinguished in this article) classes. Create SRC /exceptions. Ts with the following code:

// src/exceptions.ts
export class BaseException extends Error {
  / / status code
  status: number;
  // Prompt message
  message: string;
}

export class NotFoundException extends BaseException {
  status = 404;

  constructor(msg? :string) {
    super(a);this.message = msg || 'Nothing like this'; }}export class UnauthorizedException extends BaseException {
  status = 401;

  constructor(msg? :string) {
    super(a);this.message = msg || 'Not logged in'; }}export class ForbiddenException extends BaseException {
  status = 403;

  constructor(msg? :string) {
    super(a);this.message = msg || 'Insufficient permissions'; }}Copy the code

The type of error here references the design of Nest.js. For learning purposes, this is simplified and only implements the errors we need to use.

Use custom errors in Controller

Then we can use the custom error in the Controller. SRC /controllers/auth.ts

// src/controllers/auth.ts
// ...
import { UnauthorizedException } from '.. /exceptions';

export default class AuthController {
  public static async login(ctx: Context) {
    // ...

    if(! user) {throw new UnauthorizedException('Username does not exist');
    } else if (await argon2.verify(user.password, ctx.request.body.password)) {
      ctx.status = 200;
      ctx.body = { token: jwt.sign({ id: user.id }, JWT_SECRET) };
    } else {
      throw new UnauthorizedException('Password error'); }}// ...
}
Copy the code

As you can see, the code that manually sets the status code and response body has been changed to a simple error throw, which makes the code much cleaner.

prompt

The Koa Context object provides a convenient method called throw that can also throw an exception, such as ctx.throw(400, ‘Bad request’).

Also, modify the userController-related logic. SRC /controllers/user.ts

// src/controllers/user.ts
// ...
import { NotFoundException, ForbiddenException } from '.. /exceptions';

export default class UserController {
  // ...

  public static async showUserDetail(ctx: Context) {
    const userRepository = getManager().getRepository(User);
    const user = await userRepository.findOne(+ctx.params.id);

    if (user) {
      ctx.status = 200;
      ctx.body = user;
    } else {
      throw newNotFoundException(); }}public static async updateUser(ctx: Context) {
    const userId = +ctx.params.id;

    if(userId ! == +ctx.state.user.id) {throw new ForbiddenException();
    }

    // ...
  }
 // ...
  public static async deleteUser(ctx: Context) {
    const userId = +ctx.params.id;

    if(userId ! == +ctx.state.user.id) {throw new ForbiddenException();
    }

    // ...}}Copy the code

Add error-handling middleware

Finally, we need to add error-handling middleware to catch errors thrown in the Controller. Open SRC /server.ts to implement error-handling middleware with the following code:

// src/server.ts
// ...

createConnection()
  .then((a)= > {
    // ...

    // Register middleware
    app.use(logger());
    app.use(cors());
    app.use(bodyParser());

    app.use(async (ctx, next) => {
      try {
        await next();
      } catch (err) {
        // Only JSON responses are returned
        ctx.status = err.status || 500; ctx.body = { message: err.message }; }});// ...
  })
  // ...
Copy the code

As you can see, in this error-handling middleware, we converted the returned response data to JSON format (instead of Plain Text) for a more uniform look.

This concludes the tutorial. The content is very much, hope to have certain help to you. Our user system has been able to handle most situations, but some marginal cases are still poorly handled (can you think of any?). . But then again, I’m sure you’ve decided that Koa is a great framework?

Want to learn more exciting practical skills tutorial? Come and visit the Tooquine community.