The Fun of Functional (Part 5)
So we've got ourselves a data, transport, and business layer together and it's all working nicely. One problem we've got is that, anything we post to our endpoints could be sharp or dangerous. It's time to wrap this baby in some bubblewrap!
Validation!
Today we'll add a simple middleware to our transport layer using JSON schema validation.
Our dependencies:
npm i fastest-validator --save
- fastest validator is the fastest json schema validator. It says so on the tin! You could use ajv or yup or any other schema validator. There are a lot to choose from...
Price check
Let's start by adding some validation to the
add
endpoint inside the transport layer:
const http = require('http'); const bodyParser = require('body-parser'); const app = require('restana')(); const todo = require('./business'); const valid = require('./validation'); app.use(bodyParser.json()); app.get('/:scope', todo.browse); app.get('/:scope/:id', todo.read); app.patch('/:scope/:id', todo.edit); // add our validation middleware here app.post('/:scope', valid.todo, todo.add); app.delete('/:scope/:id', todo.del); http.createServer(app).listen(8080);
...and now our validation implementation
const Validator = require('fastest-validator'); const { send422 } = require('./utils'); const v = new Validator(); const todoSchema = { title: { type: 'string', max: 255, min: 3, alphanum: true, required: true }, $$strict: true, // no additional properties allowed }; const checkTodo = v.compile(todoSchema); const isInvalid = (v) => Array.isArray(v); const validateTodo = ({ body }) => Promise.resolve(checkTodo(body)); const maybeInvalid = (res) => (next) => (r) => isInvalid(r) ? send422(res)(r) : next(); module.exports.todo = (req, res, next) => validateTodo(req) // .then(maybeInvalid(res)(next));
A point of interest here is that I'm wrapping the validation in a promise where it's otherwise not needed. Another way to achieve the same result would be to use a third party pipe/chain function or write your own. I don't see the point when a native promise chain will give you the same result.
With the
add
endpoint done, we can use the very same validation for the edit
endpoint, we'll take a look at that later. For now let's throw some validation together for the scope
and id
path parameters:
const Validator = require('fastest-validator'); const { send422 } = require('./utils'); const v = new Validator(); const todoSchema = { title: { type: 'string', max: 255, min: 3, alphanum: true, required: true }, $$strict: true, }; // added scope schema const scopeSchema = { scope: { type: 'string', max: 32, min: 3, alphanum: true, required: true }, }; // added id schema const idSchema = { id: { type: 'string', length: 22, alphanum: true, required: true }, }; const checkTodo = v.compile(todoSchema); // added scope checker const checkScope = v.compile(scopeSchema); // added id checker const checkId = v.compile(idSchema); const isInvalid = (v) => Array.isArray(v); const validateTodo = ({ body }) => Promise.resolve(checkTodo(body)); // added scope validation const validateScope = ({ params }) => Promise.resolve(checkScope(params)); // added id validation const validateId = ({ params }) => Promise.resolve(checkId(params)); const maybeInvalid = (res) => (next) => (r) => isInvalid(r) ? send422(res)(r) : next(); module.exports.todo = (req, res, next) => validateTodo(req) // .then(maybeInvalid(res)(next)); module.exports.scope = (req, res, next) => validateScope(req) // .then(maybeInvalid(res)(next)); module.exports.id = (req, res, next) => validateId(req) // .then(maybeInvalid(res)(next));
That's pretty much it. There looks like a small abstraction opportunity, let's see how it looks:
const Validator = require('fastest-validator'); const { send422 } = require('./utils'); const v = new Validator(); const todoSchema = { title: { type: 'string', max: 255, min: 3, alphanum: true, required: true }, $$strict: true, }; const scopeSchema = { scope: { type: 'string', max: 32, min: 3, alphanum: true, required: true }, }; const idSchema = { id: { type: 'string', length: 22, alphanum: true, required: true }, }; const checkTodo = v.compile(todoSchema); const checkScope = v.compile(scopeSchema); const checkId = v.compile(idSchema); const isInvalid = (v) => Array.isArray(v); const validateTodo = ({ body }) => Promise.resolve(checkTodo(body)); const validateScope = ({ params }) => Promise.resolve(checkScope(params)); const validateId = ({ params }) => Promise.resolve(checkId(params)); const maybeInvalid = (res) => (next) => (r) => isInvalid(r) ? send422(res)(r) : next(); const validatify = (fn) => (req, res, next) => fn(req) // .then(maybeInvalid(res)(next)); module.exports.todo = validatify(validateTodo); module.exports.scope = validatify(validateScope); module.exports.id = validatify(validateId);
It's few less lines of code but it doesn't do much for readibility. Let's live with it.
Over at the transport layer, here's how that looks now:
const http = require('http'); const bodyParser = require('body-parser'); const app = require('restana')(); const todo = require('./business'); const validate = require('./validate'); app.use(bodyParser.json()); app.get('/:scope', validate.scope, todo.browse); app.get('/:scope/:id', validate.scope, validate.id, todo.read); app.patch('/:scope/:id', validate.scope, validate.id, validate.todo, todo.edit); app.post('/:scope', validate.scope, validate.todo, todo.add); app.delete('/:scope/:id', validate.scope, validate.id, todo.del); http.createServer(app).listen(8080);
Looks pretty good. I like the single responsibility and code reuse of the validators across endpoints.
Validation as middleware is awesome! It allows us to decouple from the business and data layer.
Fin
I think this API is ready for a frontend and that's what we'll tackle in the next post.