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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
WeDoctor Frontend Technology
Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
