ponds
v0.1.0
Published
An express middleware for better data and error handling per route
Readme
Ponds
An express middleware for better data and error handling per route.
Install
Usage
- Ponds
- Dispatch
- Errors
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 tonextby the controller.req: object, an expressrequestobject.res: object, an expressresponseobject.
error: function, with signature(error, req, res):error: Error, an error sent tonextby the controller.req: object, an expressrequestobject.res: object, an expressresponseobject.
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. Iftrue,ponds.get()will return an array, its first element being a handler that willnext()aNotFoundPublicError; iffalse, it will return a single final handler for thenext()'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'); // falseponds.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 expressrequestobject.res: object, an expressresponseobject.
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 expressrequestobject.res: object, an expressresponseobject.
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 predefinederrorstypes below):id: string.message: string.status: number.
additional: object, optional, with keys:info: any, optional, provides any additional information to be accessed via theinstance.infoproperty.err: Error, optional, provides the original error, of any kind, aPublicErrorhad as a cause.
Properties
id: string.pascalId: string, same asidbut replacing any letter following_for it's uppercase variant. Example: forsome_id, it'spascalIdwould besomeId.message: string.status: number.info: any.child: Error, the error passed asadditional.errto thePublicErrorinstance, if any.first: PublicError, the first (bottom) error that is an instance ofPublicErrorwhen following thechildchain.
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' });