ponds
v0.1.0
Published
An express middleware for better data and error handling per route
Downloads
14
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' });