Easy, rich and fully validated koa routing.
I'm Imed Jaberi a koa contributor and I have maintained many koa modules and I still do it. I have made a PR to bump the koa/joi-router
to v9.0.0
but it's still spooned for a long time because unavailability of the owner (@aheckmann) who owns all access permissions and the inability of the koa team to get the access for the current time and for a long time...
For that, and for the community which wants the new updates, I publish this module.
You can check the opened PR on the origin repo here.
Also, I would like to make an acknowledgment for people who help and/or follow the original PR;
- built in input validation using joi
- built in output validation using joi
- built in body parsing using co-body and await-busboy
- built on the great @koa/router
- exposed route definitions for later analysis
- string path support
- regexp-like path support
- multiple method support
- multiple middleware support
- continue on error support
- router prefixing support
- router level middleware support
- meta data support
- HTTP 405 and 501 support
Node.js >= 16
is required.
const Koa = require("koa");
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const Joi = KoaJoiRouter.Joi;
const public = new KoaJoiRouter();
public.get("/", async (ctx) => {
ctx.body = "hello joi-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);
@koa-better-modules/joi-router
returns a class which you use to define your routes.
The design is such that you construct multiple router instances, one for
each section of your application which you then add as koa middleware.
const Koa = require("koa");
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const pub = new KoaJoiRouter();
const admin = new KoaJoiRouter();
const auth = new KoaJoiRouter();
// add some routes ..
pub.get("/some/path", async () => {});
admin.get("/admin", async () => {});
auth.post("/auth", async () => {});
const app = new Koa();
app.use(pub.middleware());
app.use(admin.middleware());
app.use(auth.middleware());
app.listen();
It is HIGHLY RECOMMENDED you use this bundled version of Joi to avoid bugs related to passing an object created with a different release of Joi into the router.
const Koa = require("koa");
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const Joi = KoaJoiRouter.Joi;
Adds a new route to the router. route()
accepts an object or array of objects
describing route behavior.
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const public = new KoaJoiRouter();
public.route({
method: "post",
path: "/signup",
validate: {
header: joiObject,
query: joiObject,
params: joiObject,
body: joiObject,
maxBody: "64kb",
output: { "400-600": { body: joiObject } },
type: "form",
failure: 400,
continueOnError: false,
},
pre: async (ctx, next) => {
await checkAuth(ctx);
return next();
},
handler: async (ctx) => {
await createUser(ctx.request.body);
ctx.status = 201;
},
meta: { this: { is: "stored internally with the route definition" } },
});
or
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const public = new KoaJoiRouter();
const routes = [
{
method: "post",
path: "/users",
handler: async (ctx) => {},
},
{
method: "get",
path: "/users",
handler: async (ctx) => {},
},
];
public.route(routes);
-
method
: required HTTP method like "get", "post", "put", etc -
path
: required string -
validate
-
header
: object which conforms to Joi validation -
query
: object which conforms to Joi validation -
params
: object which conforms to Joi validation -
body
: object which conforms to Joi validation -
maxBody
: max incoming body size for forms or json input -
failure
: HTTP response code to use when input validation fails. default400
-
type
: if validating the request body, this is required. eitherform
,json
ormultipart
-
formOptions
: options for co-body form parsing whentype: 'form'
-
jsonOptions
: options for co-body json parsing whentype: 'json'
-
multipartOptions
: options for busboy parsing whentype: 'multipart'
-
any busboy constructor option. eg
{ limits: { files: 1 }}
-
autoFields
: Determines whether form fields should be auto-parsed (default:true
). See the await-busboy docs.
-
any busboy constructor option. eg
-
output
: see output validation -
continueOnError
: if validation fails, this flags determines if@koa-better-modules/joi-router
should continue processing the middleware stack or stop and respond with an error immediately. useful when you want your route to handle the error response. defaultfalse
-
validateOptions
: options for Joi validate. default{}
-
-
handler
: required async function or functions -
pre
: async function or function, will be called before parser and validators -
meta
: meta data about this route.@koa-better-modules/joi-router
ignores this but stores it along with all other route data
@koa-better-modules/joi-router
supports the traditional router.get()
, router.post()
type APIs
as well.
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const admin = new KoaJoiRouter();
// signature: router.method(path [, config], handler [, handler])
admin.put("/thing", handler);
admin.get("/thing", middleware, handler);
admin.post("/thing", config, handler);
admin.delete("/thing", config, middleware, handler);
Middleware run in the order they are defined by .use()(or .get(), etc.) They are invoked sequentially, requests start at the first middleware and work their way "down" the middleware stack which matches Express 4 API.
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const users = new KoaJoiRouter();
users.get("/:id", handler);
users.use("/:id", runThisAfterHandler);
Defines a route prefix for all defined routes. This is handy in "mounting" scenarios.
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const users = new KoaJoiRouter();
users.get("/:id", handler);
// GET /users/3 -> 404
// GET /3 -> 200
users.prefix("/users");
// GET /users/3 -> 200
// GET /3 -> 404
Defines middleware for named route parameters. Useful for auto-loading or validation.
See @koa/router
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const users = new KoaJoiRouter();
const findUser = (id) => {
// stub
return Promise.resolve("Cheddar");
};
users.param("user", async (id, ctx, next) => {
const user = await findUser(id);
if (!user) return (ctx.status = 404);
ctx.user = user;
await next();
});
users.get("/users/:user", (ctx) => {
ctx.body = `Hello ${ctx.user}`;
});
// GET /users/3 -> 'Hello Cheddar'
Generates routing middleware to be used with koa
. If this middleware is
never added to your koa
application, your routes will not work.
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const public = new KoaJoiRouter();
public.get("/home", homepage);
const app = koa();
app.use(public.middleware()); // wired up
app.listen();
The route definition for the currently matched route is available
via ctx.state.route
. This object is not the exact same route
definition object which was passed into @koa-better-modules/joi-router, nor is it
used internally - any changes made to this object will
not have an affect on your running application but is available
to meet your introspection needs.
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const public = new KoaJoiRouter();
public.get("/hello", async (ctx) => {
console.log(ctx.state.route);
});
When using the validate.type
option, @koa-better-modules/joi-router
adds a few new properties
to ctx.request
to faciliate input validation.
The ctx.request.body
property will be set when either of the following
validate.type
s are set:
- json
- form
When validate.type
is set to json
, the incoming data must be JSON. If it is not,
validation will fail and the response status will be set to 400 or the value of
validate.failure
if specified. If successful, ctx.request.body
will be set to the
parsed request input.
admin.route({
method: "post",
path: "/blog",
validate: { type: "json" },
handler: async (ctx) => {
console.log(ctx.request.body); // the incoming json as an object
},
});
When validate.type
is set to form
, the incoming data must be form data
(x-www-form-urlencoded). If it is not, validation will fail and the response
status will be set to 400 or the value of validate.failure
if specified.
If successful, ctx.request.body
will be set to the parsed request input.
admin.route({
method: "post",
path: "/blog",
validate: { type: "form" },
handler: async (ctx) => {
console.log(ctx.request.body); // the incoming form as an object
},
});
The ctx.request.parts
property will be set when either of the following
validate.type
s are set:
- multipart
When validate.type
is set to multipart
, the incoming data must be multipart data.
If it is not, validation will fail and the response
status will be set to 400 or the value of validate.failure
if specified.
If successful, ctx.request.parts
will be set to an
await-busboy object.
admin.route({
method: "post",
path: "/blog",
validate: { type: "multipart" },
handler: async (ctx) => {
const parts = ctx.request.parts;
let part;
try {
while ((part = await parts)) {
// do something with the incoming part stream
part.pipe(someOtherStream);
}
} catch (err) {
// handle the error
}
console.log(parts.field.name); // form data
},
});
Note: if you do not specify a value for validate.type
, the
incoming payload will not be parsed or validated. It is up to you to
parse the incoming data however you see fit.
admin.route({
method: "post",
path: "/blog",
validate: {},
handler: async (ctx) => {
console.log(ctx.request.body, ctx.request.parts); // undefined undefined
},
});
Validating the output body and/or headers your service generates on a per-status-code basis is supported. This comes in handy when contracts between your API and client are strict e.g. any change in response schema could break your downstream clients. In a very active codebase, this feature buys you stability. If the output is invalid, an HTTP status 500 will be used.
Let's look at some examples:
router.route({
method: "post",
path: "/user",
validate: {
output: {
200: {
// individual status code
body: {
userId: Joi.string(),
name: Joi.string(),
},
},
},
},
handler: handler,
});
router.route({
method: "post",
path: "/user",
validate: {
output: {
"200,201": {
// multiple individual status codes
body: {
userId: Joi.string(),
name: Joi.string(),
},
},
},
},
handler: handler,
});
router.route({
method: "post",
path: "/user",
validate: {
output: {
"200-299": {
// status code range
body: {
userId: Joi.string(),
name: Joi.string(),
},
},
},
},
handler: handler,
});
You are free to mix and match ranges and individual status codes.
router.route({
method: "post",
path: "/user",
validate: {
output: {
"200,201,300-600": {
// mix it up
body: {
userId: Joi.string(),
name: Joi.string(),
},
},
},
},
handler: handler,
});
Validating your output headers is also supported via the headers
property:
router.route({
method: "post",
path: "/user",
validate: {
output: {
"200,201": {
body: {
userId: Joi.string(),
name: Joi.string(),
},
headers: Joi.object({
// validate headers too
authorization: Joi.string().required(),
}).options({
allowUnknown: true,
}),
},
"500-600": {
body: {
// this rule only runs when a status 500 - 600 is used
error_code: Joi.number(),
error_msg: Joi.string(),
},
},
},
},
handler: handler,
});
Each router exposes it's route definitions through it's routes
property.
This is helpful when you'd like to introspect the previous definitions and
take action e.g. to generate API documentation etc.
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const admin = new KoaJoiRouter();
admin.post("/thing", { validate: { type: "multipart" } }, handler);
console.log(admin.routes);
// [ { path: '/thing',
// method: [ 'post' ],
// handler: [ [Function] ],
// validate: { type: 'multipart' } } ]
Sometimes you need RegExp
-like syntax support for your route definitions.
Because path-to-regexp
supports it, so do we!
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const admin = new KoaJoiRouter();
admin.get(
"/blog/:year(\\d{4})-:day(\\d{2})-:article(\\d{3})",
async (ctx, next) => {
console.log(ctx.request.params); // { year: '2017', day: '01', article: '011' }
}
);
Defining a route for multiple HTTP methods in a single shot is supported.
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const admin = new KoaJoiRouter();
admin.route({
path: "/",
method: ["POST", "PUT"],
handler: fn,
});
Often times you may need to add additional, route specific middleware to a single route.
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const admin = new KoaJoiRouter();
admin.route({
path: "/",
method: ["POST", "PUT"],
handler: [yourMiddleware, yourHandler],
});
You may want to bundle and nest middleware in different ways for reuse and organization purposes.
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const admin = new KoaJoiRouter();
const commonMiddleware = [yourMiddleware, someOtherMiddleware];
admin.route({
path: "/",
method: ["POST", "PUT"],
handler: [commonMiddleware, yourHandler],
});
This also works with the .get(),post(),put(),delete(), etc HTTP method helpers.
const KoaJoiRouter = require("@koa-better-modules/joi-router");
const admin = new KoaJoiRouter();
const commonMiddleware = [yourMiddleware, someOtherMiddleware];
admin.get("/", commonMiddleware, yourHandler);
By default, @koa-better-modules/joi-router
stops processing the middleware stack when either
input validation fails. This means your route will not be reached. If
this isn't what you want, for example, if you're writing a web app which needs
to respond with custom html describing the errors, set the validate.continueOnError
flag to true. You can find out if validation failed by checking ctx.invalid
.
admin.route({
method: "post",
path: "/add",
validate: {
type: "form",
body: {
id: Joi.string().length(10),
},
continueOnError: true,
},
handler: async (ctx) => {
if (ctx.invalid) {
console.log(ctx.invalid.header);
console.log(ctx.invalid.query);
console.log(ctx.invalid.params);
console.log(ctx.invalid.body);
console.log(ctx.invalid.type);
}
ctx.body = await render("add", { errors: ctx.invalid });
},
});
-
npm test
runs tests + code coverage + lint -
npm run lint
runs lint only -
npm run lint-fix
runs lint and attempts to fix syntax issues -
npm run test-cov
runs tests + test coverage -
npm run open-cov
opens test coverage results in your browser -
npm run test-only
runs tests only