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

typemania

v2.0.1

Published

Create middleware that will automatically respond with a 400 status code and a detailed error message if the request body does not match the expected type definitions.

Downloads

49

Readme

typemania

There are many libraries helping you validate that an email is an email and a postal code is a postal code. That's great, we need those too.

But first an foremost, as API creators, we want to provide the consumers of our APIs with predictable and human readable error messages:

{
  "status": 400,
  "message": "Bad request",
  "errors": {
    "firstName": {
      "type": "missing",
      "message": "expected string, got undefined"
    },
    "lastName": {
      "type": "invalid",
      "message": "expected string, got number"
    },
    "devices": [
      {
        "index": 0,
        "type": "missing",
        "message": "expected object, got undefined"
      },
      {
        "index": 2,
        "notificationPriority": {
          "type": "invalid",
          "message": "expected number, got string"
        }
      }
    ],
    "skills": {
      "canCook": {
        "type": "missing",
        "message": "expected boolean, got undefined"
      }
    },
    "weakApi": {
      "type": "invalid",
      "message": "expected string || number || shape, got object"
    }
  }
}

Before diving into the nitty gritty, we want to be able to simply tackle 95% of the bad requests, without breaking a sweat.

We only want to make sure that an email is a valid email if we have an otherwise complete payload. ie: All the required fields are populated, and whatever data we get is of the expected type.

What this library offers is a simple way to create complex validation logic on an API route in a Node.js app. It responds automatically to a Bad request with a 400 status code and detailed feedback.

Most importantly, embracing middleware philosophy, it allows you to craft route handlers that can safely assume they will only ever be called with a valid payload.

You can always call additional middleware to validate the incoming payload further if you need to. The advantage of using this library higher up the middleware stack, is that middleware functions below it can focus on more granular validation details, without having to worry about the presence of a value or its type. (ie: your email validator no longer needs to check if a value is a non-empty string, it can strictly focus on the more relevant logic that makes an email an email)

Here is how it would look like if you built your API with Express:

  1. Create a custom validator:
// foodieValidator.js
const { createValidator, type } = require('typemania').express;

const foodieValidator = createValidator(type.shape({
  firstName: type.string.isRequired,
  age: type.number,
  visitedRestaurants: type.arrayOf(type.shape({
    name: type.string.isRequired,
    address: type.object,
    liked: type.boolean.isRequired,
  })).isRequired,
  favoriteRestaurants: type.arrayOf(type.string),
  noIdea: type.arrayOf(type.oneOf([ type.string, type.number ]))
}));

module.exports = foodieValidator;
  1. Use it like you would any middleware:
// server.js
const express = require('express');
const foodieValidator = require('./path/to/foodieValidator.js');

const router = express.Router();
router.post('/foodnerds', foodieValidator, (req, res) => {
  // fearless destructuring happens here
  res.send('yum');
});

type

Largely inspired by the Proptypes library from the React ecosystem... just, reduced to the bare minimum to describe a JSON payload.

const { type } = require('typemania');

type.string (and any other JSON primitives)

// optional string
type.string
// required string
type.string.isRequired

type.shape vs type.object

Validate the received value is an object literal of any shape:

type.object
type.object.isRequired

Validate the received value is an object literal of a specific shape:

// optional object with required property "age" and optional property "isYoung"
type.shape({
  age: type.number.isRequired,
  isYoung: type.boolean
})

Complex objects:

// required object with nested objects
type.shape({
  firstName: type.string.isRequired,
  address: type.shape({
    main: type.string,
    postalCode: type.string.isRequired,
  }),
  vagueOtherThings: type.object,
}).isRequired

type.arrayOf vs type.array

Validate the received value is an array of elements of any type:

type.array
type.array.isRequired

Validate the received value is an array of elements of a specific type:

// [ '',, null]
type.arrayOf(type.string) // allow null and undefined values
// [ '', '']
type.arrayOf(type.string.isRequired) // strictly strings

Complex arrays:

// required array of arrays [[], [], []]
type.arrayOf(type.array).isRequired
// optional array of arrays of objects
// [
//   [{ nested: [{}], arrays: [] }],
//   [{ nested: [{}], arrays: [] }, { nested: [{}], arrays: [] }]
// ]
type.arrayOf(type.arrayOf(type.shape({
  nested: type.arrayOf(type.object),
  arrays: type.array,
})))

type.oneOf

// Either a string or a number
type.oneOf([type.string, type.number])

Complex array example with oneOf:

// [
//   { "prop1": "val1" },
//   5,
//   true,
//   "if that's really what you want..."
// ]
type.arrayOf(type.oneOf([
  type.shape({ prop1: type.string }),
  type.number,
  type.boolean,
  type.string,
]));

If you need to guarantee the type of an element at a specific index within the array, you might want to reconsider your API specs first.

The library does not offer a type.any as it would go against its design philosophy. But if you must, there are ways...

The library does not offer a type.exact. Instead of forcing the consumers of your API to provide an exact value, consider setting defaults in your API code.

The single concern of this library is a value's type, not the value itself. It cannot help you validate that a value is either a 5 or an 8, solely that it is indeed a number.

Advanced

I'm actually interested, but I don't use Express

createFrameworkValidator

As long as the framework you work with has a clearly defined middleware signature, you can make your own validators generator with createFrameworkValidator.

Express middleware functions have the following signature: (req, res, next) => {}

So here is how createExpressValidator is made under the hood:

const createExpressValidator = (typeDefinitions) => (req, res, next) => createFrameworkValidator({
  validate: typeDefinitions,
  payload: req.body,
  handleBadRequest: (error) => res.status(error.status).send(error),
  handleValidRequest: next,
});

This means to create your own makeValidator you will write something like this:

const { createFrameworkValidator } = require('typemania');

/**
 * @param {*} typeDefinitions ex: type.shape({ firstName: type.string }).isRequired
 * @returns {Function} A middleware function
 */
const makeValidator = (typeDefinitions) => {
  // here "todo" is a placeholder
  // for whatever the signature of a middleware function is
  // in the framework that you work with
  return function middleware(todo) {
    return createFrameworkValidator({
      validate: typeDefinitions,
      payload: todo, // retrieve the payload from the request the way your framework dictates
      handleBadRequest: (error) => {
        // const { status, message, errors } = error;
        todo; // send response and end the request the way your framework dictates
      }, // called if payload is invalid
      handleValidRequest: todo, // the next middleware or route handler to be called if a payload is valid
    });
  };
};

module.exports = makeValidator;

Which you would use like:

const makeValidator = require('./path/to/makeValidator.js');

const myThingValidator = makeValidator(type.shape({
  myProp1: type.string,
  myProp2: type.number,
  etc: type.arrayOf(type.object)
}));
// and then just plug the myThingValidator middleware in the routes that need gatekeeping
// the way your framework dictates