(Updated: )
/ #micro #node #javascript 

Simple, but not too simple: how using Zeit’s `micro` improves your Node applications

Leave the Express comfort zone to expand how you think about Node application architecture.

tl;dr

  • using a function composition model for building HTTP servers is awesome
  • Functions as a Service are great but have some drawbacks
  • micro has a similar, simple API to FaaS but doesn’t have a compilation step
  • micro’s minimalism affects how you solve problems

An example comparing micro and express can be found at: github.com/HugoDF/micro-vs-express-example.

Table of contents:

Table of Contents

Functions as a composition model for JavaScript

Here are some of my application development beliefs that are relevant to this post:

  • Functions are awesome
  • An optimal solution is simple, but not too simple
  • javascript’s most powerful feature is first-class functions

From the above follows that in JavaScript, composing functions tends to be the a good way to build a solution that’s simple to reason about but built of standard blocks.

That’s one of the reasons why “Functions as a Service” (FaaS), also called “serverless” platforms are attractive for Node developers. As part of building Post Frequenc, the initial approach was to write the backend on Netlify’s lambda platform. At the prototype stage, it worked, there were 2 transactional lambda functions:

  • one to get a feed URL from a site URL
  • one to parse an RSS/Atom feed from a URL.

Netlify Lambdas (which actually end up deployed as AWS lambdas) have a straightforward API, event in, data out (using a callback or an async function).

I realised that the best way to handle the input is a feed URL and input is a site URL dichotomy was to just try fetch as both and see what errors and what doesn’t. ie. I built something like this:

const coerceErrorToObjectProperty = promise =>
  promise.catch(error => Promise.resolve({ error }));
  
function handler () {
  // deal with input
  const url = "some-url";
  Promise.all([
    coerceErrorToObjectProperty(parseFeed(url)),
    coerceErrorToObjectProperty(getFeedUrl(url))
  ])
    .then(([feedOutput, feedUrlOutput]) => {
      if (!feedOutput.error) {
        console.log('');
        return feedOutput;
      }
      if (!feedUrlOutput.error) {
        console.log('');
        return feedUrlOutput;
      }
      throw new Error(feedOutput.error || feedUrlOutput.error);
    })
    .then(() => {
      // deal with response
    })
    .catch(() => {
      // deal with error
    });
}

Functions as a Service pain points

At this point I hit one of my first developer experience issues running on FaaS:

  1. my backend now needs to be bundled
  2. debugging minified code is still not nice

This is beyond the cold-start and timeout issues that are usually used to argue against the FaaS route.

With widespread support for Node 8+, there’s less and less reason to transpile your backend code (even if you want to use ES Module syntax, see ES Modules, see Use ES Modules in Node without Babel/Webpack).

Beyond Express: micro as a library to write simple Node applications

Hopefully this section doesn’t come off as too much of an “Express vs micro" debate.

I love Express for its stability, maturity and flexibility, it’s has been and still is a true work horse for the Node.js server ecosystem.

micro is more focused and built with fewer constraints (eg. it can leverage async/await support in Node)

After writing lambdas I was looking for a backend framework that had an API as self-contained as a lambda. That library is micro. Which lead to the following reflections:

  • I have experience with Express but I feel like it has a bit too much friction for tiny apps, which this would be. As most people who have used Express know, you have to install extra packages for middleware behaviour like body-parsing, this comes out of the box with micro.
  • To send a response in Express, you still use callback syntax: res.json(data) or res.send(data).
  • A middleware-based extension system is cool but it’s not always as explicit as you would want it to be. The request/response handler sometimes relies on a property being set by an arbitrary middleware up the middleware stack.

The above points are literally some of micro’s selling points (see https://github.com/zeit/micro#features):

  • Easy: Designed for usage with async and await (more)
  • Simple: Oriented for single purpose modules (function)
  • Standard: Just HTTP!
  • Explicit: No middleware - modules declare all dependencies

It also has the following extra’s that are marginal gains for me compared to Express (again from https://github.com/zeit/micro#features):

  • Fast: Ultra-high performance (even JSON parsing is opt-in)
  • Micro: The whole project is ~260 lines of code
  • Agile: Super easy deployment and containerization
  • Lightweight: With all dependencies, the package weighs less than a megabyte

With the following counterpoints in my opinion:

  • Express is “fast enough”
  • Express is “small enough” (even though sometimes running Express in Nodemon inside of Docker leads to 1s+ reload times)
  • Express is “easy enough to containerise/deploy”
  • Express is “lightweight enough” (an Express app + dependencies is seldom more than 10s of megabytes, compared to Rails or Django apps which easily hit 50-100+ megabytes)

It comes with body-parsing baked in but not much else. Which is a good thing, it keeps to its name.

Here are equivalent apps that respond to a POST that sends a number and increments it by 1 (simple and semi-useless but hey):

  • Express in an express-app.js file:

      const express = require('express');
      const bodyParser = require('body-parser');
      const app = express();
      app.use(bodyParser.json());
      app.post('/', (req, res) => {
        const { value = 0 } = req.body;
        return res.json({
          value: Number(value) + 1
        });
      });
      app.listen(process.env.PORT || 3000, () => {
        console.log('Server listening on PORT', process.env.PORT || 3000);
      });
    
  • micro in a micro-app.js file:

    const { json, createError } = require('micro');
    module.exports = async (req) => {
      if (req.method !== 'POST') {
        throw createError(404, 'Not Found');
        // to have the same behaviour as the Express app
      }
      const { value = 0 } = await json(req);
      return {
        value: Number(value) + 1
      };
    };
    

The package.json looks like the following:

{
  "main": "micro-app.js",
  "scripts": {
    "express": "node express-app.js",
    "micro": "micro"
  },
  "dependencies": {
    "body-parser": "^1.18.3",
    "express": "^4.16.4",
    "micro": "^9.3.3"
  }
}

You can find this working example on GitHub: github.com/HugoDF/micro-vs-express-example.

How micro helps your architecture

Mapping my lambdas into a micro app I used [fs-router](https://github.com/jesseditson/fs-router) to have 2 routes, one for input is a site URL OR input is a feed URL and one for input is a feed URL.

What happened when adding fs-router was interesting.

In micro, you have to put effort to have routes. This effort is similar to the effort required in Express to have body parsing. Which illustrates what each of these tools considers core to the problem they’re solving vs something that can be done but isn’t the core problem.

micro’s problem is around building simple, explicit and a large number of (micro)services. This is why routing is not in the core: with microservices, with the right interfaces a chunk of services might not require routing.

express takes the opposite view, it’s designed to be flexible for any server application on Node. From building simple one-route applications to REST APIs to building fully-featured, single-deployment web applications with server-rendered templates. With this in mind it goes about solving two core problems: routing and a standardised plugin system (based on middleware).

For this project, this friction made me realise that having two endpoints which have duplicated functionality is crazy: input is a feed URL is already covered by input is a site URL OR input is a feed URL. So I deleted the second route, removed fs-router and deployed 👍.

Another feature of micro that I want to illustrate is the following:

const applyMiddlewares = handler =>
  health(
    log(
      cors(
        rateLimit(
          handler
        )
      )
    )
);
module.exports = applyMiddlewares((req, res) => 
  'Service with /health, logging, CORS and rate-limiting'
);

“middleware” is just a set of functions, that are composed and applied to a handler (instead of the more opaque app.use(() => {}) or app.get(``'``/path``'``, middleware1, middleware2, handler)). The handler defines what augmentations is wants/needs instead of expecting the top-level app to provide them.

The micro docs, ecosystem and examples

Micro’s docs reflect the library’s philosophy: they’re clean and simple and showcase the few core use-cases and actions, the following is similar to what’s at https://github.com/zeit/micro#usage: This is the hello world, app.js

module.exports = () => 'Hello world';

You can set the following package.json:

{
  "main": "app.js",
  "scripts": {
    "start": "micro"
  },
  "dependencies": {
    "micro": "latest"
  }
}

Which can then be run with npm start.

Further information

I’ll be writing up some of the details of how I deploy my micro service(s) and Express apps, there’s already a teaser on that topic: Deployment options: Netlify + Dokku on DigitalOcean vs now.sh, GitHub Pages, Heroku and AWS.

Feel free to tweet at me @hugo__df.

unsplash-logoKelly Sikkema

Author

Hugo Di Francesco

Co-author of "Professional JavaScript", "Front-End Development Projects with Vue.js" with Packt, "The Jest Handbook" (self-published). Hugo runs the Code with Hugo website helping over 100,000 developers every month and holds an MEng in Mathematical Computation from University College London (UCL). He has used JavaScript extensively to create scalable and performant platforms at companies such as Canon, Elsevier and (currently) Eurostar.

Get The Jest Handbook (100 pages)

Take your JavaScript testing to the next level by learning the ins and outs of Jest, the top JavaScript testing library.