Skip to content

Instantly share code, notes, and snippets.

@ephys
Last active October 9, 2016 20:25
Show Gist options
  • Save ephys/dac46d2f2e63c53ec08b5153ef976d17 to your computer and use it in GitHub Desktop.
Save ephys/dac46d2f2e63c53ec08b5153ef976d17 to your computer and use it in GitHub Desktop.

My ideal HTTP server

Using ESNext to make building http server less tedious:

  • Add Promise and async/await support.
  • Use decorators to set the routes metadata rather than writing the same boilerplate in every route handler.
  • Use Flow or TypeScript for type inference (maybe).

Promises and Async/Await

Promises are everywhere, but most http server frameworks completely lack support for them. Forcing you to remember to handle all rejections to pass them to the callback, as they would be silently ignored if you don't.

import express from 'express';

const app = express();

app.get('/user/:userid', (request, response, next) => {
  const userId = getUserId(request);

  database.users
    .findOneById(userId)
    .then(user => {
      response.json(user);
    }).catch(e => {
      next(e);
    });
});

Instead, the server framework could check if the function returned a promise and handle possible rejections there.

app.get('/user/:userid', (request, response) => {
  const userId = getUserId(request);

  return database.users
    .findOneById(userId)
    .then(user => {
      response.json(user);
    });
});

Which would, furthermore, enable async/await support. All possible rejections would automatically be caught and properly handled by the framework.

HTTP requests wouldn't be stuck forever anymore if you forgot to call response.send as the promise resolving would automatically cause the response to be sent. (Alternatively, this could be considered an error and throw).

app.get('/user/:userid', async (request, response) => {
  const userId = getUserId(request);

  const user = await database.users.findOneById(userId);
  response.json(user);
});

Further exploit promises to send data as a response.

Rather than having to call response.send(), simply return the value that you wish to send. For instance, the previous code could be written like this:

app.get('/user/:userid', async (request) => {
  const userId = getUserId(request);

  const user = await database.users.findOneById(userId);
  return JSON.stringify(user);
});

It also works with templating engines:

app.get('/user/:userid', async (request) => {
  const userId = getUserId(request);

  const user = await database.users.findOneById(userId);
  return userProfileTemplate.render({ user });
});

And just like that, you don't need to keep a reference to request and response everywhere anymore. Although it makes it more difficult to eagerly send the response to the browser.

## Use decorators for metadata.

This is something that a lot of frameworks from other languages already do. I personally really like it as it separates the (un-)serializing logic from the business logic, without having to write yet-another-layer in my app.

Again, the previous code could be rewritten like this:

import { GET } from 'express/decorators';

// ...

@GET('/user/:userid', app)
async function renderUser(request) {
  const userId = getUserId(request);

  const user = await database.users.findOneById(userId);
  return userProfileTemplate.render({ user });
};

But that's not all. It could also be used as an alternative to routers, by using classes. (My Java background is leaking, but not /all/ of it is terrible).

Router version:

const apiRoutes = express.Router();

apiRoutes.get('/users/:userid', (request, response) => {
  // ...
});

app.use('/api/v2.5', apiRoutes);

Decorators version:

import { Routes, GET } from 'express/decorators';

@Routes('/api/v2.5', app)
class ApiRoutes {

  @GET('/user/:userid')
  async function renderUser(request) {
    // ...
  };
}

To be honest, this is more of a nice to have than anything and I'm sure some of you are currently thinking that I'm a complete idiot for wanting such a feature. But if you're already using projects like sequelize-decorators, you'll probably like having the rest of your codebase look similar.

Use decorators for dependency injection.

Still there ? Good, we're not done with decorators yet.

One point that I deeply dislike when writing routes is correctly handling parameters; be it query, body, header or path parameters. And I certainly don't want to skip this step as I don't want to have to deal with the possible subtle bugs that would come with manipulating strings as if they were numbers.

Plus, the HTTP error code to return depends on what kind of parameter you're receiving. And - unless you're sloppy when it comes to HTTP - you will need to correctly set which one to use in your response.

As a result, my code ends up looking like this:

app.get('/user/:userid/messages/:friendid', (request, response) => {
  const userId = Number(request.params.userid);
  const friendId = Number(request.params.friendid);
  const limit = request.query.limit ? Number(request.query.limit) : Number.POSITIVE_INFINITY;

  if (Number.isNaN(userId) || Number.isNaN(friendId)) {
    // bad path param, should return a 404.
    throw new NotFoundException();
  }

  if (Number.isNaN(limit)) {
    // bad query param, should return 400
    throw new BadRequestException();
  }

  // ... check if the users exist, 404 otherwise, return their messages.
});

Writing that kind of code for every single route is tedious and I hate it. I ended up writing my own helper functions of course but I don't really consider that ideal.

app.get('/user/:userid/messages/:friendid', (request, response) => {
  const user = DataExtractors.param.getUnsignedInteger(request, 'user');
  const penfriend = DataExtractors.param.getUnsignedInteger(request, 'penfriend');
  const limit = DataExtractors.query.getUnsignedInteger(request, 'limit', /* optional */ true) || Number.POSITIVE_INFINITY;

  // ... check if the users exist, 404 otherwise, return their messages.
});

Instead of doing that, we could use decorators for dependency injection and data extraction.
The previous code could be rewritten like this:

import { GET, Path, Query } from 'express/decorators';
import { uint32 } from 'express/extractors';

@GET('/user/:userid/messages/:friendid', app)
async function getMessages(
  @Path('userid', uint32) userId,
  @Path('friendid', uint32) friendId,
  @Query('limit', {
    parser: uint32,
    default: Number.POSITIVE_INFINITY
  }) limit,
) {
  // ... check if the users exist, 404 otherwise, return their messages.
}

Such a system would enable the framework to implement checks that the path param name is correctly defined and used. Something like the following could produce an error to point out to the developer that they misnomed the parameter.

@GET('/user/:userid', app)
async function getMessages(@Path('user', uint32) userId) {}

Notice how we don't even need Request and Response as parameters anymore and the usual (request, response, next) signature has been replaced by a dependency injection system that gives to the method just what it needs. Of course we could inject these variables by using parameter decorators such as @Request and @Response.

And we can go even further with custom extractors and directly fetch the entity.

// File: extractors.js
import { uint32 } from 'express/extractors';
import database from './database';
import User from './models/User';

export async function user(param) {
  if (param instanceof User) {
    return param;
  }

  const uid = uint32(param);
  const user = await database.users.findOneById(uid);

  if (user == null) {
    throw new ExtractorException();
  }

  return user;
}

// File: index.js
import { GET, Path, Query } from 'express/decorators';
import { uint32 } from 'express/extractors';
import { user as userExtractor } from './extractors';

@GET('/user/:userid/messages/:friendid', app)
async function getMessages(
  @Path('userid', userExtractor) user,
  @Path('friendid', userExtractor) friend,
  @Query('limit', {
    parser: uint32,
    default: Number.POSITIVE_INFINITY
  }) limit,
) {

}

Exploiting the type system

The biggest problem with this approach is how verbose the parameter definition becomes. We could likely enhance it a bit with a type system like flow or TypeScript. For instance:

import { GET, Path, Query } from 'express/decorators';
import { uint32 } from 'express/extractors';
import { user as userExtractor } from './extractors';
import User from './models/User';

app.registerExtractor(User, userExtractor);

@GET('/user/:userid/messages/:friendid', app)
async function getMessages(
  @Path('userid') user: User,
  @Path('friendid') friend: User,
  @Query('limit', {
    parser: uint32, // if this one isn't specified, the framework will use the `Number` parser that accepts any number rather than unsigned 32 bit integers.
    default: Number.POSITIVE_INFINITY
  }) limit: Number,
) {

}

## Stringifying responses

This section is a bit of a WIP.

So far, the only acceptable output is strings, which is good for regular templating apps but not so great if you're writing a REST Api.

Now this is up for debate, but I don't want to accidentally return an object and have it being serialized and sent to the browser. So my proposed solution to this problem is to make the framework produce an error if we return something else than a string, unless the method (or the class the method is in) is tagged with @Serialize.

The serialization behavior would be as follows:

  1. If the @Serialize decorator has a function argument, delegate to that function then go to step 3.
  2. If the app's serializer registry has an entry for the item to serialize, delegate to that entry then go to step 3.
  3. Stringify the item based on the request's Accept header and return it.

Step 1 and 2 allow the developers to define custom methods for serializing their data. These methods do not especially need to stringify the data as that will be handled by the step 3.

So a serializer can receive an User object, call .toJSON on it, delete the password field, and return the resulting object. Step 3 will then handle stringifying to one of the requested formats based on the Accept header (XML, JSON, HTML, etc).

import { GET, Path } from 'express/decorators';
import { user as userExtractor } from './extractors';
import { serializerThatDeletesPasswordFields } from './serializers';
import User from './models/User';

app.registerExtractor(User, userExtractor);
app.registerSerializer(User, serializerThatDeletesPasswordFields);

@Serialize() // alternatively, put it as an argument for this decorator.
@GET('/user/:userid', app)
function getUser(@Path('userid') user: User) {
  return user;
}

Note: serializers are great for APIs that need to return raw data but aren't usable for templating as the resulting item will be stringified.

Summary

One day maybe, we'll be able to choose between writing this:

import express from 'express';
import database from './database';

const app = express();

app.get('/user/:userid', (request, response, next) => {
  const userId = Number(request.params.userid);
  if (!userId) {
    throw new NotFoundException();
  }

  database.users
    .findOneById(userId)
    .then(user => {
      if (!user) {
        throw new NotFoundException();
      }

      delete user.password;

      response.json(user);
    })
    .catch(e => next(e));
});

or writing this:

import { GET, Path } from 'express/decorators';
import { user as userExtractor } from './extractors';
import { serializerThatDeletesPasswordFields } from './serializers';
import User from './models/User';

const app = express();

app.registerExtractor(User, userExtractor);
app.registerSerializer(User, serializerThatDeletesPasswordFields);

@Serialize()
@GET('/user/:userid', app)
function getUser(@Path('userid') user: User) {
  return user;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment