ponds

An express middleware for better data and error handling per route

Usage no npm install needed!

<script type="module">
  import ponds from 'https://cdn.skypack.dev/ponds';
</script>

README

Ponds

Version Build Status Coverage Dependencies Vulnerabilities Issues License

An express middleware for better data and error handling per route.

Install

npm install ponds

Usage

Basics

Each pond corresponds with a way of handling data and errors for a set of your endpoints, and are supposed to handle the response for two scenarios: success and error.

Let's define a couple ponds for two different API response formats:

File: ponds-setup.js

import ponds from 'ponds';

ponds.set('api_1', {
  data(data, req, res) {
    res.json({
      status: 'success',
      data: data
    })
  },
  error(err, req, res) {
    res.json({
      status: 'error',
      error: err
    })
  }
});

ponds.set('api_2', {
  data(data, req, res) {
    res.json({
      data: data,
      error: null
    })
  },
  error(err, req, res) {
    res.json({
      data: null,
      error: err
    })
  }
});

Once defined, we can use them in our routes:

File: app.js

import express from 'express';
import ponds from 'ponds';
import routes from './routes';
import './ponds-setup';

const app = express();

// By default, ponds includes a 404 handler that ships a
// NotFound PublicError (see errors and PublicError)
app.use(routes, ponds.get('api_2'));

// In this case, as we're not doing app.use() for a set of routes,
// we can't have the NotFound error, so we passe `false`as a second param.
app.get('/myRoute', (req, res, next) => {
  // Instead of sending the response,
  // we send our data to next().
  // If it's an error, it'll be handled by our error handler;
  // otherwise it'll go through our data handler
  next({ some: 'data', foo: 'else' });
}, ponds.get('api_1', false));

Hence, if we send a get request to /myRoute, as it has a next pond api_1, we'd get: { error: null, data: { some: 'data', foo: 'else' } }.

File: routes.js

import { Router } from 'express';

const router = Router();

router.get('/routes/foo', (req, res, next) => {
  // Instead of sending the response,
  // we send our data to next().
  // If it's an error, it'll be handled by our error handler;
  // otherwise it'll go through our data handler
  next({ other: 'data', foo: 'else' });
})

export default router;

Hence, as all routes in routes.js have a next pond api_2 middleware, if we send a get request to /routes/foo, we'd get: { status: 'success', data: { other: 'data', foo: 'else' } }.

Ponds

ponds.set(name, handler)

Sets a pond handler.

  • name: string, the name of the handler.
  • handler: object, with keys:
    • data: function, with signature (data, req, res):
      • data: any, the data sent to next by the controller.
      • req: object, an express request object.
      • res: object, an express response object.
    • error: function, with signature (error, req, res):
      • error: Error, an error sent to next by the controller.
      • req: object, an express request object.
      • res: object, an express response object.
import ponds from 'ponds';

ponds.set('api_1', {
  data(data, req, res) {
    res.json({
      status: 'success',
      data: data
    })
  },
  error(err, req, res) {
    res.json({
      status: 'error',
      error: err
    })
  }
});

ponds.get(name, notFound?)

Returns a previously set handler as an express middleware.

  • name: string, the name of the handler.
  • notFound: boolean, optional. If true, ponds.get() will return an array, its first element being a handler that will next() a NotFound PublicError; if false, it will return a single final handler for the next()'ed data/error. Default: true.
import ponds from 'ponds';

app.get('/myRoute', (req, res, next) => {
  next({ some: 'data', foo: 'else' });
}, ponds.get('api_1', false));

ponds.exists(name)

Returns true if a pond has been set, false otherwise.

  • name: string, the name of the handler.
import ponds from 'ponds';

ponds.exists('api_1'); // true
ponds.exists('foo_pond'); // false

ponds.transform(transform)

Sets a data/error transform that will execute before the next()'ed data is received by any pond handler. It's particularly useful to reformat errors from different libraries to a PublicError in order for them to be handled by the pond error handler.

  • transform: object, with keys:
    • data: function, optional, receives and should return any data.
    • error: function, optional, receives and should return an error.
import ponds, { PublicError, errors } from 'ponds';

ponds.transform({
  error(err) {
    if (err instanceof SomeDbLibError) {
      return new PublicError(errors.Database, { err });
    }
    return err;
  }
});

Dispatch

dispatch(cb)

Wraps a middleware function sending to next() any returned data and catching any thrown errors (also sent to next()).

  • cb: function, with signature (req, res):
    • req: object, an express request object.
    • res: object, an express response object.
import ponds, { dispatch, PublicError, errors } from 'ponds';

const controller = dispatch((req, res) => {
  if (!req.body.somethingRequired) {
    throw new PublicError(
      errors.RequestValidation,
      { info: `Request didn't have "somethingRequired".` }
    );
  }
  return {
    some: 'data',
    foo: 'else'
  };
});

app.get('/myRoute', controller, ponds.get('api_1', false));

dispatch.all(obj)

Same as dispatch() for an object of functions.

  • obj: object, with any number of keys and values of functions, with signature (req, res):
    • req: object, an express request object.
    • res: object, an express response object.
import ponds, { dispatch, PublicError, errors } from 'ponds';

const controllers = dispatch.all({
  myRoute(req, res) {
    if (!req.body.somethingRequired) {
      throw new PublicError(
        errors.RequestValidation,
        { info: `Request didn't have "somethingRequired".` }
      );
    }
    return {
      some: 'data',
      foo: 'more data'
    };
  },
  otherRoute(req, res) {
    return { some: 'other', foo: 'else' };
  }
});

app.get('/myRoute', controller.myRoute, ponds.get('api_1', false));
app.get('/otherRoute', controllers.otherRoute, ponds.get('api_1', false));

Errors

This library includes an Error type to handle additional information regarding the status code, information, and stack.

PublicError

  • new PublicError(type, additional?)

    • type: object, with keys (see the predefined errors types below):
      • id: string.
      • message: string.
      • status: number.
    • additional: object, optional, with keys:
      • info: any, optional, provides any additional information to be accessed via the instance.info property.
      • err: Error, optional, provides the original error, of any kind, a PublicError had as a cause.
  • Properties

    • id: string.
    • pascalId: string, same as id but replacing any letter following _ for it's uppercase variant. Example: for some_id, it's pascalId would be someId.
    • message: string.
    • status: number.
    • info: any.
    • child: Error, the error passed as additional.err to the PublicError instance, if any.
    • first: PublicError, the first (bottom) error that is an instance of PublicError when following the child chain.

Because you can store the origin errors a PublicError had as a cause (which can be another PublicError), it'd be possible to throw several PublicErrors in a chain, and access the first via the last thrown.

import { dispatch, PublicError, errors } from 'ponds';

async function service() {
  // let's assume something happened
  throw Error('Something happened');
}

async function dbQuery() {
  try {
    return service();
  } catch(err) {
    throw new PublicError(errors.Database, { err });
  }
}

async function controller() {
  try {
    dbQuery()
  } catch(err) {
    throw new PublicError(errors.Server, { err });
  }
}

try {
  controller();
} catch(err) {
  // `err` would be a Server PublicError
  // `err.first` would be a Database PublicError
  // `err.first.child` would be an Error with message "Something happened"
}

errors

ponds exports an object with some predefined error types:

  • Server: { id: 'server', message: 'Server error', status: 500 },
  • NotFound: { id: 'not_found', message: 'Server Not Found', status: 404 },
  • Unauthorized: { id: 'unauthorized', message: "You don't have access to this resource", status: 401 },
  • RequestValidation: { id: 'request_validation', message: 'Invalid request', status: 400 },
  • Database: { id: 'database', message: 'Database error', status: 500 },
  • DatabaseValidation: { id: 'database_validation', message: 'Invalid database request', status: 500 },
  • DatabaseNotFound: { id: 'database_not_found', message: 'Item not found in database', status: 500 }
import { PublicError, errors } from 'ponds';

new PublicError(errors.Server, { info: 'Some additional information' });