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

@reactor4/forklift

v2.3.0

Published

Express.js middleware for handling request and response validation, serialization and more

Downloads

306

Readme

Forklift

Build Status

Forklift is a TypeScript library written to simplify request-response flow in Express.js applications. It is meant to be used as a pre & post core logic middleware.

Forklift's IO in action

Example usage

IO

First, let's import the IO module.

import { IO } from "@reactor4/forklift";

IO is a class with both static functions and instance methods which are used in conjuction to ease the process of data validation and manipulation throughout express.js request and response flow.

To be able to validate the data against something, IO class needs to be initialized with a JSON Schema. More info about JSON Schema specification can be found here.

const exampleSchema = {
  $schema: "http://json-schema.org/draft-07/schema#",
  title: "Example Schema",
  description: "Schema for creating examples",
  type: "object",
  properties: {
    exampleId: {
      type: "number",
    },
    exampleName: {
      type: "string",
    },
  },
  additionalProperties: false,
  required: ["exampleId", "exampleName"],
};

const exampleIo = new IO({ reqBodySchema: exampleSchema });

Now, ioInstance can be included as a middleware just like any other.

// This is a express.js router

router.post(
  "/example",
  exampleIo.processRequest(),
  // any other business logic, e.g.
  exampleController.post
);

Note that processRequest method actually returns a function that accepts regular express.js middleware arguments (req, res, next). That function executes as a middleware and validates the req argument. Validation starts with headers as the Accept and Content-Type headers are required.

Request is expected to provide the following headers:

Accept: application/json
Content-Type: application/json, */*, *

Next, the request content is validated (req.body) against the provided schema at the IO class constructor's first parameter. For example, the exampleSchema above expects request's body to provide an object with exactly two properties; exampleId and exampleName, of types number and string, respectively.

If the headers or body do not comply with the specification, an error is thrown inside the module which is expected to be handled explicitly.

Nevertheless, if the request passes the validation, the request's body is stored under locals.io.data namespace inside the req middleware object. Forklift actually provides additional static functions that make the locals namespace easy to manipulate with.

For example,

static set(target: object, data: any, status: Status = Status.OK, path: string = null)

is the most general one which sets the object at locals.io.data.${path} to the provided data argument, as well as setting locals.io.status to the status value (enum which can be imported from Forklift).

Such approach is expected when setting the response data so Forklift can know what data to validate and serialize.

// exampleController.js
import { IO, Status } from "@reactor4/forklift";

post(req, res, next) {
  ...
  // business logic
  ...

  IO.set(response, data, Status.CREATED);
  // or simply
  IO.setCreated(response, data);
}

To conclude the pipeline and send the response another middleware method is required.

router.post(
  "/example",
  exampleIo.processRequest(),
  // any other business logic, e.g.
  exampleController.post,
  exampleIo.sendResponse()
);

It is also possible to validate query parameters if reqQuerySchema is provided in IOs config constructor. The options parameter object for now includes possibility to include other valid Content-Type headers. The final middleware function sendResponse will validate the response data (located at res object's locals.io.data namespace) if response JSON schema was provided as resBodySchema parameter in IO class constructor.

const exampleIo = new IO(
  {
    reqBodySchema,
    reqQuerySchema, 
    options: { contentTypes: ["text/html"] }, 
    resBodySchema 
  }
);

Middleware & Errors

Two basic middleware functions that come with Forklift provide out of the box pipeline error handling with clean code base in mind.

import { errorMiddleware } from "@reactor4/forklift";

Let's start with the more straight forward one, errorMiddleware. This is simply a Forklift implementation of an error handler that is expected to be included at the end of the Express.js pipeline.

// e.g.
app.use(someRouter);
app.use(someOtherRouter);
app.use(errorMiddleware());

Expected behavior of this middleware is to handle special type of errors which extend ForkliftError class. It is designed to provide a possibilty to add custom errors without much hassle. Every error that is a subclass of ForkliftError has toJson method available which will provide a pretty formatted object of error details.

import ForkliftError from '@reactor4/forklift';

class ForbiddenError extends ForkliftError {
  constructor(message: string) {
    // ForkliftError's constructor expects message, status of the response to be written, and an error name
    super(message, 403, 'Forbidden');
  }
}

In the example above, Forklift's implementation of ForbiddenError can be seen. Same pattern can be reused for any other custom error type. Forklift comes with some common HTTP error types ready.

import { BadRequestError, ForbiddenError, NotFoundError, ConflictError } from "@reactor4/forklift";

To be able to use async/await promise syntax and still use an error handler like described Forklift's middleware, without wrapping each of your await's inside a try/catch block Forklift provides a higher order function which can be used to wrap pipeline handlers.

import { asyncMiddleware, BadRequestError, ForbiddenError } from "@reactor4/forklift";

  getItems() {
    return asyncMiddleware(async (req: Request, res: Response) => {
      if (!req.params.size) {
        throw new BadRequestError(`"Size" query parameter not provided!`);
      }

      const isAuthorized = await checkUser(req.headers);

      if (!isAuthorized) {
        throw new ForbiddenError("User is unauthorized for this action.");
      }

      const items = await getXAmountOfItems(req.params.size);

      IO.set(res, items);
    });
  }

asyncMiddleware basically wraps the function argument inside a try/catch block and calls the next function after it's completed.