Mastering Koa: A Step‑by‑Step Guide to Building Scalable Backend Services

This comprehensive tutorial walks you through choosing Koa, setting up a project, refactoring routing, handling parameters, configuring middleware for error handling, CORS, logging, validation, and organizing config, controller, service, and model layers for a robust Node.js backend.

WeDoctor Frontend Technology
WeDoctor Frontend Technology
WeDoctor Frontend Technology
Mastering Koa: A Step‑by‑Step Guide to Building Scalable Backend Services

Why Choose Koa

Because Koa is lightweight, has almost no built‑in features, and offers high flexibility, making it ideal for developers who like to experiment.

For a full example, see the repository: https://github.com/JustGreenHand/koa-app

Set Up the Project and Start the Service

After the basic setup, the directory structure looks like this:

In app/index.js add the simplest server code:

const Koa = require('koa');

const app = new Koa();
const port = '8082';
const host = '0.0.0.0';

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(port, host, () => {
  console.log(`API server listening on ${host}:${port}`);
});

Run node app/index.js and visit http://localhost:8082 to see the response.

Refactor Routing

When the project grows, routing logic becomes harder to maintain. Move routing code from app/index.js to a dedicated router file using koa-router:

const Koa = require('koa');
const router = require('../router');

const app = new Koa();
const port = '8082';
const host = '0.0.0.0';

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(port, host, () => {
  console.log(`API server listening on ${host}:${port}`);
});

Create app/router/index.js:

const KoaRouter = require('koa-router');
const router = new KoaRouter();

const routeList = require('./routes');

routeList.forEach(item => {
  const { method, path, controller } = item;
  router[method](path, controller);
});

module.exports = router;

Define routes in app/router/routes.js:

const { test } = require('../controllers');

const routes = [
  {
    method: 'get',
    path: '/a',
    controller: test.list
  }
];

module.exports = routes;

Implement a simple controller in app/controllers/test.js:

const list = async ctx => {
  ctx.body = '路由改造后的结果';
};

module.exports = { list };

Parameter Parsing

POST requests initially could not read body. Adding koa-bodyparser as middleware solves this:

const koaBody = require('koa-bodyparser');

const mdKoaBody = koaBody({
  enableTypes: ['json', 'form', 'text', 'xml'],
  formLimit: '56kb',
  jsonLimit: '1mb',
  textLimit: '1mb',
  xmlLimit: '1mb',
  strict: true
});

For file uploads, formidable is introduced. app/middlewares/formidable.js:

const Formidable = require('formidable');
const { tempFilePath } = require('../config');

module.exports = () => {
  return async (ctx, next) => {
    const form = new Formidable({
      multiples: true,
      uploadDir: `${process.cwd()}/${tempFilePath}`
    });
    await new Promise((resolve, reject) => {
      form.parse(ctx.req, (err, fields, files) => {
        if (err) reject(err);
        else {
          ctx.request.body = fields;
          ctx.request.files = files;
          resolve();
        }
      });
    });
    await next();
  };
};

Combine both parsers in app/middlewares/index.js:

const koaBody = require('koa-bodyparser');
const formidable = require('./formidable');

const mdFormidable = formidable();
const mdKoaBody = koaBody({
  enableTypes: ['json', 'form', 'text', 'xml'],
  formLimit: '56kb',
  jsonLimit: '1mb',
  textLimit: '1mb',
  xmlLimit: '1mb',
  strict: true
});

module.exports = [mdFormidable, mdKoaBody];

Unified Response Format & Error Handling

Create app/middlewares/response.js to attach ctx.res.success and ctx.res.fail helpers:

module.exports = () => {
  return async (ctx, next) => {
    ctx.res.fail = ({ code, data, msg }) => {
      ctx.body = { code, data, msg };
    };
    ctx.res.success = (msg) => {
      ctx.body = { code: 0, data: ctx.body, msg: msg || 'success' };
    };
    await next();
  };
};

Define error handling in app/middlewares/error.js:

module.exports = () => {
  return async (ctx, next) => {
    try {
      await next();
      if (ctx.status === 200) ctx.res.success();
    } catch (err) {
      if (err.code) {
        ctx.res.fail({ code: err.code, msg: err.message });
      } else {
        ctx.app.emit('error', err, ctx);
      }
    }
  };
};

Register both middlewares together with routing:

const mdResHandler = require('./response')();
const mdErrorHandler = require('./error')();
const mdRoute = router.routes();
const mdRouterAllowed = router.allowedMethods();

module.exports = [mdFormidable, mdKoaBody, mdResHandler, mdErrorHandler, mdRoute, mdRouterAllowed];

Add a global error listener in app/index.js:

app.on('error', (err, ctx) => {
  if (ctx) {
    ctx.body = {
      code: 9999,
      message: `程序运行时报错:${err.message}`
    };
  }
});

CORS Configuration

Use @koa/cors middleware:

const cors = require('@koa/cors');
const mdCors = cors({
  origin: '*',
  credentials: true,
  allowMethods: ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']
});

module.exports = [mdFormidable, mdKoaBody, mdCors, mdResHandler, mdErrorHandler, mdRoute, mdRouterAllowed];

Logging with log4js

Configure log4js to write request logs:

const log4js = require('log4js');
const { outDir, flag, level } = require('../config').logConfig;

log4js.configure({
  appenders: { cheese: { type: 'file', filename: `${outDir}/receive.log` } },
  categories: { default: { appenders: ['cheese'], level: 'info' } },
  pm2: true
});

const logger = log4js.getLogger();
logger.level = level;

module.exports = () => {
  return async (ctx, next) => {
    const { method, path, origin, query, body, headers, ip } = ctx.request;
    const data = { method, path, origin, query, body, ip, headers };
    await next();
    if (flag) {
      const { status, params } = ctx;
      data.status = status;
      data.params = params;
      data.result = ctx.body || 'no content';
      if (ctx.body.code !== 0) logger.error(JSON.stringify(data));
      else logger.info(JSON.stringify(data));
    }
  };
};

Insert the logger into the middleware chain:

const mdLogger = require('./log')();
module.exports = [mdFormidable, mdKoaBody, mdCors, mdLogger, mdResHandler, mdErrorHandler, mdRoute, mdRouterAllowed];

Parameter Validation

Create a validator factory app/middlewares/paramValidator.js that uses @hapi/joi schemas:

module.exports = paramSchema => {
  return async (ctx, next) => {
    let body = ctx.request.body;
    try { if (typeof body === 'string' && body.length) body = JSON.parse(body); } catch (_) {}
    const paramMap = { router: ctx.request.params, query: ctx.request.query, body };
    if (!paramSchema) return next();
    const schemaKeys = Object.getOwnPropertyNames(paramSchema);
    if (!schemaKeys.length) return next();
    schemaKeys.some(item => {
      const validObj = paramMap[item];
      const validResult = paramSchema[item].validate(validObj, { allowUnknown: true });
      if (validResult.error) {
        ctx.utils.assert(false, ctx.utils.throwError(9998, validResult.error.message));
      }
    });
    await next();
  };
};

Define schemas in app/schema/test.js:

const Joi = require('@hapi/joi');

module.exports = {
  list: {
    query: Joi.object({
      name: Joi.string().required(),
      age: Joi.number().required()
    })
  }
};

Wire the validator into routing ( app/router/index.js):

const KoaRouter = require('koa-router');
const router = new KoaRouter();
const routeList = require('./routes');
const paramValidator = require('../middlewares/paramValidator');

routeList.forEach(item => {
  const { method, path, controller, valid } = item;
  router[method](path, paramValidator(valid), controller);
});

module.exports = router;

Update the route definition to reference the schema ( app/router/routes.js):

const { test } = require('../controllers');
const { scmTest } = require('../schema');

module.exports = [
  {
    method: 'get',
    path: '/a',
    valid: scmTest.list,
    controller: test.list
  }
];

Database Operations (Outline)

Introduce a service layer for data‑access logic and a model layer for defining table structures. Controllers call services, keeping business logic separate from database queries.

Summary

The guide demonstrates how to build a clean, maintainable Koa backend by progressively adding middleware for routing, parameter parsing, unified responses, error handling, CORS, logging, and validation, while organizing code into controllers, services, models, and configuration files.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

middlewareloggingRoutingError handlingKoa
WeDoctor Frontend Technology
Written by

WeDoctor Frontend Technology

Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.