curveball / core

The Curveball framework is a TypeScript framework for node.js with support for modern HTTP features.
https://curveballjs.org/
MIT License
526 stars 7 forks source link
curveball typescript

Curveball

Curveball is a framework for building web services in Node.js. It fullfills a similar role to Express and it's heavily inspired by Koa.

This web framework has the following goals:

If you used Koa in the past, this is going to look pretty familiar. I'm a big fan of Koa myself and would recommend it over this project if you don't need any of the things this project offers.

Installation

npm install @curveball/core

Getting started

Curveball only provides a basic framework. Using it means implementing or using curveball middleware. For example, if you want a router, use or build a Router middleware.

All of the following examples are written in typescript, but it is also possible to use the framework with plain javascript.

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

const app = new Application();
app.use((ctx: Context) => {

  ctx.status = 200;
  ctx.response.body = 'Hello world!'

});

app.listen(4000);

Middlewares you might want

Authentication

You might like a12n-server, a full OAuth2 authorization server, written in Curveball and works well with the OAuth2 middleware.

AWS Lambda support / 'Serverless'

Bun support

To use Curveball with Bun, use the kernel package:

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

const app = new Application();

// Add all your middlewares here!
app.use( ctx => {
  ctx.response.body = {msg: 'hello world!'};
});

export default {
  port: 3000,
  fetch: app.fetch.bind(app)
};

Some more details can be found in this article.

Doing internal subrequests

Many Node.js HTTP frameworks don't easily allow doing internal sub-requests. Instead, they recommend doing a real HTTP request. These requests are more expensive though, as it has to go through the network stack.

Curveball allows you to do an internal request with 'mock' request and response objects.

Suggested use-cases:

Example:

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

const app = new Application();
const response = await app.subRequest('POST', '/foo/bar', { 'Content-Type': 'text/html' }, '<h1>Hi</h1>');

Only the first 2 arguments are required. It's also possible to pass a Request object instead.

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

const app = new Application();
const request = new MemoryRequest('POST', '/foo/bar', { 'Content-Type': 'text/html' }, '<h1>Hi</h1>');
const response = await app.subRequest(request);

HTTP/2 push

HTTP/2 push can be used to anticipate GET requests client might want to do in the near future.

Example use-cases are:

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

const app = new Application();
const server = http2.createSecureServer({
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem')
}, app.callback());
server.listen(4443);

app.use( ctx => {

  ctx.response.status = 200;
  ctx.response.headers.set('Content-Type', 'text/html');
  ctx.response.body = '';

  await ctx.response.push( pushCtx => {

    pushCtx.path = '/script.js';
    return app.handle(pushCtx);

  });

});

HTTP/2 push works by sending HTTP responses to the client, but it also includes HTTP requests. This is because HTTP clients need to know which request the response belongs to.

The push function simply takes a middleware, similar to use on Application. The callback will only be triggered if the clients supports push and wants to receive pushes.

In the preceding example, we are using app.handle() to do a full HTTP request through all the regular middlewares.

It's not required to do this. You can also generate responses right in the callback or call an alternative middleware.

Lastly, pushCtx.request.method will be set to GET by default. GET is also the only supported method for pushes.

Sending 1xx Informational responses

Curveball has native support for sending informational responses. Examples are:

Here's an example of a middleware using 103 Early Hints:

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

const app = new Curveball();
app.use(async (ctx: Context, next: Middleware) => {

  await ctx.response.sendInformational(103, {
    'Link' : [
      '</style.css> rel="prefetch" as="style"',
      '</script.js> rel="prefetch" as="script"',
    ]
  });
  await next();

});

Websocket

To get Websocket up and running, just run:

app.listenWs(port);

This will start a websocket server on the specified port. Any incoming Websocket connections will now just work.

If a Websocket connection was started, the Context object will now have a webSocket property. This property is simply an instance of Websocket from the ws NPM package.

Example usage:

import { UpgradeRequired } from '@curveball/http-errors';

app.use( ctx => {
  if (!ctx.webSocket) {
    throw new UpgradeRequired('This endpoint only supports WebSocket');
  }

  ctx.webSocket.send('Hello');
  ctx.webSocket.on('message', (msg) => {
    console.log('Received %s', msg);
  });

});

If you use typescript, install the @types/ws package to get all the correct typings:

npm i -D @types/ws

The Controller package also has built-in features to make this even easier.

API

The Application class

The application is main class for your project. It's mainly responsible for calling middlewares and hooking into the HTTP server.

It has the following methods

The Context class

The Context object has the following properties:

The Request interface

The Request interface represents the HTTP request. It has the following properties and methods:

The Response interface

The Response interface represents a HTTP response. It has the following properties and methods:

The Headers interface

The Headers interface represents HTTP headers for both the Request and Response.

It has the following methods:

Other features

Use the checkConditional function to verify the following headers:

Signature:

checkConditionial(req: RequestInterface, lastModified: Date | null, etag: string | null): 200 | 304 : 412;

This function returns 200 if the conditional passed. If it didn't, it will return either 304 or 412. The former means you'll want to send a 304 Not Modified back, the latter 412 Precondition Failed.

200 does not mean you have to return a 200 OK status, it's just an easy way to indicate that all all conditions have passed.