DEV Community

Evert Pot
Evert Pot

Posted on • Originally published at evertpot.com

Curveball - A typescript microframework

Since mid-2018 we've been working on a new micro-framework, written in typescript. The framework competes with Express, and takes heavy inspiration from Koa. It's called Curveball.

If you only ever worked with Express, I feel that for most people this project will feel like a drastic step up. Express was really written in an earlier time of Node.js, before Promises and async/await were commonplace, so first and foremost the biggest change is the use of async/await middlewares throughout.

If you came from Koa, that will already be familiar. Compared to Koa, these are the major differences:

  • Curveball is written in Typescript
  • It has strong built-in support HTTP/2 push.
  • Native support for running servers on AWS Lambda, without the use of strange hacks.
  • Curveball's request/response objects are decoupled from the Node.js http library.

At Bad Gateway we've been using this in a variety of (mostly API)
projects for the past few years, and it's been working really well for us.
We're also finding that it tends to be a pretty 'sticky' product. People exposed to it tend to want to use it for their next project too.

Curious? Here are a bunch of examples of common tasks:

Examples

Hello world

import { Application } from '@curveball/core';

const app = new Application();
app.use( async ctx => {
  ctx.response.type = 'text/plain';
  ctx.response.body = 'hello world';
});

app.listen(80);
Enter fullscreen mode Exit fullscreen mode

Everything is a middleware, and middlewares may or may not be async.

Hello world on AWS Lambda

import { Application } from '@curveball/core';
import { handler } from '@curveball/aws-lambda';

const app = new Application();
app.use( ctx => {
  ctx.response.type = 'text/plain';
  ctx.response.body = 'hello world';
});

exports.handler = handler(app);
Enter fullscreen mode Exit fullscreen mode

HTTP/2 Push

const app = new Application();
app.use( ctx => {
  ctx.response.type = 'text/plain';
  ctx.body = 'hello world';

  ctx.push( pushCtx => {

    pushCtx.path = '/sub-item';
    pushCtx.response.type = 'text/html';
    pushCtx.response.body = '<h1>Automatically pushed!</h1>';

  });


});
Enter fullscreen mode Exit fullscreen mode

The callback to ctx.push will only get called if Push was supported by the client, and because it creates a new 'context', any middleware can be attached to it, ot even all the middleware by doing a 'sub request'.

Resource-based controllers

Controllers are optional and opinionated. A single controller should only ever manage one type of resource, or one route.

import { Application, Context } from '@curveball/core';
import { Controller } from '@curveball/controller';

const app = new Application();

class MyController extends Controller {

  get(ctx: Context) {

    // This is automatically triggered for GET requests

  }

  put(ctx: Context) {

    // This is automatically triggered for PUT requests

  }

}

app.use(new MyController());
Enter fullscreen mode Exit fullscreen mode

Routing

The recommended pattern is to use exactly one controller per route.

import { Application } from '@curveball/core';
import router from '@curveball/router';

const app = new Application();

app.use(router('/articles', new MyCollectionController());
app.use(router('/articles/:id', new MyItemController());
Enter fullscreen mode Exit fullscreen mode

Content-negotation in controllers

import { Context } from '@curveball/core';
import { Controller, method, accept } from '@curveball/controller';

class MyController extends Controller {

  @accept('html')
  @method('GET')
  async getHTML(ctx: Context) {

    // This is automatically triggered for GET requests with
    // Accept: text/html

  }

  @accept('json')
  @method('GET')
  async getJSON(ctx: Context) {

    // This is automatically triggered for GET requests with
    // Accept: application/json

  }

}
Enter fullscreen mode Exit fullscreen mode

Emitting errors

To emit a HTTP error, it's possible to set ctx.status, but easier to just throw a related exception.

function myMiddleware(ctx: Context, next: Middleware) {

  if (ctx.method !== 'GET') {
    throw new MethodNotAllowed('Only GET is allowed here');
  }
  await next();

}
Enter fullscreen mode Exit fullscreen mode

The project also ships with a middleware to automatically generate RFC7807 application/problem+json responses.

Transforming HTTP responses in middlewares

With express middlewares it's easy to do something before a request was handled, but if you ever want to transform a response in a middleware, this can only be achieved through a complicated hack.

This is due to the fact that responses are immediately written to the TCP sockets, and once written to the socket it's effectively gone.

So to do things like gzipping responses, Express middleware authors needs to mock the response stream and intercept any bytes sent to it. This can be clearly seen in the express-compression source:
https://github.com/expressjs/compression/blob/master/index.js.

Curveball does not do this. Response bodies are buffered and available by
middlewares.

For example, the following middleware looks for a HTTP Accept header of
text/html and automatically transforms JSON to a simple HTML output:

app.use( async (ctx, next) => {

  // Let the entire middleware stack run
  await next();

  // HTML encode JSON responses if the client was a browser.
  if (ctx.accepts('text/html') && ctx.response.type ==== 'application/json') {
    ctx.response.type = 'text/html';
    ctx.response.body = '<h1>JSON source</h1><pre>' + JSON.stringify(ctx.response.body) + '</pre>';
  }

});
Enter fullscreen mode Exit fullscreen mode

To achieve the same thing in express would be quite complicated.

You might wonder if this is bad for performance for large files. You would be completely right, and this is not solved yet.

However, instead of writing directly to the output stream the intent for this is to allow users to set a callback on the body property, so writing the body will not be buffered, just deferred. The complexity of implementing these middlewares will not change.

HTML API browser

Curveball also ships with an API browser that automatically transforms
JSON in to traversable HTML, and automatically parses HAL links and HTTP Link
headers.

Every navigation element is completely generated based on links found in the
response.

To use it:

import { halBrowser } from 'hal-browser';
import { Application } from '@curveball/core';

const app = new Application();
app.use(halBrowser());
Enter fullscreen mode Exit fullscreen mode

Once set up, your API will start rendering HTML when accessed by a browser.

HAL browser example

Sending informational responses

ctx.response.sendInformational(103, {
  link: '</foo>; rel="preload"'
})
Enter fullscreen mode Exit fullscreen mode

Parsing Prefer headers

const foo = ctx.request.prefer('return');
// Could be 'representation', 'minimal' or false
console.log(foo);
Enter fullscreen mode Exit fullscreen mode

Installation and links

Installation:

npm i @curveball/core
Enter fullscreen mode Exit fullscreen mode

Documentation can be found on Github. A list of middlewares can be seen in the organization page.

Stable release

We're currently on the 11th beta, and closing in on a stable release. Changes will at this point be minor.

If you have thoughts or feedback on this project, it would be really helpful to hear. Don't hesitate to leave comments, questions or suggestions as a Github Issue.

A big thing that's still to be done is the completion of the website. We got a great design, it just needs to be pushed over the finish line.

One more thing?

Apologies for the cliché header. We're also working on an Authentcation server, written in curveball. It handles the following for you:

  • Login
  • Registration
  • Lost password
  • OAuth2:
    • client_credentials, password, authorization_code grant types.
    • revoke, introspect support
  • TOTP (Google authenticator style)
  • User management, privilege management.

The project needs some love in the user experience department, but if you're stick of creating another authentication system and don't want to break the bank, a12n-server might be for you.

The ultimate goal here is to create a great headless authentication server, and compete with OAuth0 and Okta, but we can use some more people power here!

Top comments (6)

Collapse
 
rfgamaral profile image
Ricardo Amaral

Looks interesting, thank you for sharing!

Any planned support for a GraphQL server or will you rely on the community for that?

What about additional middlewares for Netlify, ZEIT and Firebase?

Collapse
 
evert profile image
Evert Pot

It would be interesting to create a middleware for easily hooking up GraphQL, I would probably hope that someone contributes this.

As for the other suggestions, I feel that these types of things should probably be solved outside the framework. Integrating with those services doesn't really feel like something a microframework should do. We're really just provide plumbing for HTTP =)

Collapse
 
rfgamaral profile image
Ricardo Amaral

You did it for AWS, that's why I asked. Unless I misunderstood.

Thread Thread
 
evert profile image
Evert Pot

Ah if all of these also have serverless functions, then yes it would definitely possible to make implementations for their APIs. I was probably the one who misunderstood. Haven't used these platforms much.

Thread Thread
 
rfgamaral profile image
Ricardo Amaral

Yes, all of them have serverless functions, that's why I asked :)

Collapse
 
rfgamaral profile image
Ricardo Amaral

I'm personally using Fastify and not Express. I'm wondering how does Curveball compare to Fastify?