npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@w72/joi-router

v1.0.1

Published

Configurable, input validated routing for koa.

Downloads

6

Readme

joi-router

Easy, rich and fully validated koa routing.

Changes made by w72

  • fix case of koa
  • move koa and joi to peerDependencies
  • recommend import joi direct from joi package

Notes about the module and the current sversion:

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;

Features:

Node compatibility

Node.js >= 16 is required.

Example

import Koa from "koa";
import Joi from "joi";
import JoiRouter from "@w72/joi-router";

const public = new JoiRouter();

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);

Usage

@w72/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.

import Koa from "koa";
import JoiRouter from "@w72/joi-router";

const pub = new JoiRouter();
const admin = new JoiRouter();
const auth = new JoiRouter();

// 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();

Module properties

.Joi

It is NOT RECOMMENDED you use this bundled version of Joi

import Koa from "koa";
import Joi from "joi";
import JoiRouter from "@w72/joi-router";

Router instance methods

.route()

Adds a new route to the router. route() accepts an object or array of objects describing route behavior.

import JoiRouter from "@w72/joi-router";
const public = new JoiRouter();

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

import JoiRouter from "@w72/joi-router";
const public = new JoiRouter();

const routes = [
  {
    method: "post",
    path: "/users",
    handler: async (ctx) => {},
  },
  {
    method: "get",
    path: "/users",
    handler: async (ctx) => {},
  },
];

public.route(routes);
.route() options
  • 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. default 400
    • type: if validating the request body, this is required. either form, json or multipart
    • formOptions: options for co-body form parsing when type: 'form'
    • jsonOptions: options for co-body json parsing when type: 'json'
    • multipartOptions: options for busboy parsing when type: 'multipart'
    • output: see output validation
    • continueOnError: if validation fails, this flags determines if @w72/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. default false
    • 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. @w72/joi-router ignores this but stores it along with all other route data

.get(), post(), put(), delete() ...etc - HTTP methods

@w72/joi-router supports the traditional router.get(), router.post() type APIs as well.

import JoiRouter from "@w72/joi-router";
const admin = new JoiRouter();

// 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);

.use()

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.

import JoiRouter from "@w72/joi-router";
const users = new JoiRouter();

users.get("/:id", handler);
users.use("/:id", runThisAfterHandler);

.prefix()

Defines a route prefix for all defined routes. This is handy in "mounting" scenarios.

import JoiRouter from "@w72/joi-router";
const users = new JoiRouter();

users.get("/:id", handler);
// GET /users/3 -> 404
// GET /3 -> 200

users.prefix("/users");
// GET /users/3 -> 200
// GET /3 -> 404

.param()

Defines middleware for named route parameters. Useful for auto-loading or validation.

See @koa/router

import JoiRouter from "@w72/joi-router";
const users = new JoiRouter();

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'

.middleware()

Generates routing middleware to be used with koa. If this middleware is never added to your koa application, your routes will not work.

import JoiRouter from "@w72/joi-router";
const public = new JoiRouter();

public.get("/home", homepage);

const app = koa();
app.use(public.middleware()); // wired up
app.listen();

Additions to ctx.state

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 @w72/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.

import JoiRouter from "@w72/joi-router";
const public = new JoiRouter();

public.get("/hello", async (ctx) => {
  console.log(ctx.state.route);
});

Additions to ctx.request

When using the validate.type option, @w72/joi-router adds a few new properties to ctx.request to faciliate input validation.

ctx.request.body

The ctx.request.body property will be set when either of the following validate.types are set:

  • json
  • form

json

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
  },
});

form

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
  },
});

ctx.request.parts

The ctx.request.parts property will be set when either of the following validate.types are set:

  • multipart

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
  },
});

Handling non-validated input

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 output

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:

Validation of an individual status code

router.route({
  method: "post",
  path: "/user",
  validate: {
    output: {
      200: {
        // individual status code
        body: {
          userId: Joi.string(),
          name: Joi.string(),
        },
      },
    },
  },
  handler: handler,
});

Validation of multiple individual status codes

router.route({
  method: "post",
  path: "/user",
  validate: {
    output: {
      "200,201": {
        // multiple individual status codes
        body: {
          userId: Joi.string(),
          name: Joi.string(),
        },
      },
    },
  },
  handler: handler,
});

Validation of a status code range

router.route({
  method: "post",
  path: "/user",
  validate: {
    output: {
      "200-299": {
        // status code range
        body: {
          userId: Joi.string(),
          name: Joi.string(),
        },
      },
    },
  },
  handler: handler,
});

Validation of multiple individual status codes and ranges combined

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,
});

Validation of output headers

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,
});

Router instance properties

.routes

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.

import JoiRouter from "@w72/joi-router";
const admin = new JoiRouter();

admin.post("/thing", { validate: { type: "multipart" } }, handler);

console.log(admin.routes);
// [ { path: '/thing',
//     method: [ 'post' ],
//     handler: [ [Function] ],
//     validate: { type: 'multipart' } } ]

Path RegExps

Sometimes you need RegExp-like syntax support for your route definitions. Because path-to-regexp supports it, so do we!

import JoiRouter from "@w72/joi-router";
const admin = new JoiRouter();

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' }
  }
);

Multiple methods support

Defining a route for multiple HTTP methods in a single shot is supported.

import JoiRouter from "@w72/joi-router";
const admin = new JoiRouter();

admin.route({
  path: "/",
  method: ["POST", "PUT"],
  handler: fn,
});

Multiple middleware support

Often times you may need to add additional, route specific middleware to a single route.

import JoiRouter from "@w72/joi-router";
const admin = new JoiRouter();

admin.route({
  path: "/",
  method: ["POST", "PUT"],
  handler: [yourMiddleware, yourHandler],
});

Nested middleware support

You may want to bundle and nest middleware in different ways for reuse and organization purposes.

import JoiRouter from "@w72/joi-router";
const admin = new JoiRouter();

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.

import JoiRouter from "@w72/joi-router";
const admin = new JoiRouter();

const commonMiddleware = [yourMiddleware, someOtherMiddleware];
admin.get("/", commonMiddleware, yourHandler);

Handling errors

By default, @w72/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 });
  },
});

Development

Running tests

  • 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

LICENSE

MIT