The Fun of Functional (Part 5)

by Rob Zoszak
08 June 20224 min read

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.

Sign up and stay in the loop!

Your email address

No spam and no sharing of your details. Just useful thoughts and ideas in your inbox. :)