A zero-dependency, strongly-typed web framework for Bun, Node and Cloudflare workers

Overview

nbit

A simple, declarative, type-safe way to build web services and REST APIs for Bun, Node and Cloudflare Workers.

Examples

See some quick examples below and be sure to check out the live demo.

Bun
import { createApplication } from '@nbit/bun';

const { defineRoutes, attachRoutes } = createApplication();

const routes = defineRoutes((app) => [
  app.get('/', (request) => {
    return { hello: 'world' };
  }),
]);

Bun.serve({
  port: 3000,
  fetch: attachRoutes(routes),
});
Node
import http from 'http';
import { createApplication } from '@nbit/node';

const { defineRoutes, attachRoutes } = createApplication();

const routes = defineRoutes((app) => [
  app.get('/', (request) => {
    return { hello: 'world' };
  }),
]);

const server = http.createServer(attachRoutes(routes));

server.listen(3000, () => {
  console.log(`Server running at http://localhost:3000/`);
});
Cloudflare Workers
import { createApplication } from '@nbit/cfw';

const { defineRoutes, attachRoutes } = createApplication();

const routes = defineRoutes((app) => [
  app.get('/', (request) => {
    return { hello: 'world' };
  }),
]);

export default {
  fetch: attachRoutes(routes),
};
Node with Express
import express from 'express';
import { createApplication } from '@nbit/express';

const { defineRoutes, attachRoutes } = createApplication();

const routes = defineRoutes((app) => [
  app.get('/', (request) => {
    return { hello: 'world' };
  }),
]);

const app = express();
const middleware = attachRoutes(routes);
app.use(middleware);

app.listen(3000, () => {
  console.log(`Server running at http://localhost:3000/`);
});
Node with Apollo GraphQL
// Adapted from: https://www.apollographql.com/docs/apollo-server/api/apollo-server/#framework-specific-middleware-function
import http from 'http';
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { createApplication } from '@nbit/express';

const { defineRoutes, attachRoutes } = createApplication();

const routes = defineRoutes((app) => [
  app.get('/', (request) => {
    return { hello: 'world' };
  }),
]);

async function startApolloServer() {
  const app = express();
  const httpServer = http.createServer(app);
  const apolloServer = new ApolloServer({
    /* ... */
  });

  await apolloServer.start();

  // Additional middleware can be mounted at this point to run before Apollo.
  app.use(attachRoutes(routes));

  // Mount Apollo middleware here.
  apolloServer.applyMiddleware({ app });

  httpServer.listen(3000, () => {
    console.log(`Server running at http://localhost:3000/`);
  });
}

Objectives

  • Simplicity - providing a clean, minimal declarative API for routing and request handling based on web standards
  • Strong type guarantees - extensively leverages modern TypeScript features not just for type safety but for an all-around great developer experience
  • Testability - route handlers should be as easy to test as they are to write
  • First-class support for Bun, Node and Cloudflare workers
  • Nano-sized with no dependencies
Core Features
  • Declarative request routing
  • Effortless body parsing
  • A type-safe approach to middleware (inspired by Apollo Server's context)
  • File Serving (with content-type selection, caching headers, ETag, etc)
  • Sensible, convenient defaults and conventions
  • Extensive use of TypeScript (e.g. type inference for route params)
  • Based on web standards you're already familiar with
Work-in-progress / Coming Soon
  • Schemas and validation for JSON request body
  • Additional body parsers such as multipart/form-data
  • Tooling to statically generate an OpenAPI schema from a set of route handlers
  • High performance trie-based (e.g. radix3) request router
  • Better documentation
  • Performance benchmarks and comparisons with other libraries
  • Deno support

Motivation

I've often wanted cleaner, more declarative, type-safe tooling for building web APIs. Something that's light-weight enough to be used with Cloudflare workers and powerful enough to replace Express. Now with new performance-focused runtimes like Bun this is ever more relevant.

Aren't there already enough choices for writing web APIs? How is this different from what's out there? Read more about the motivation behind this package (aka what's wrong with Express).

Installation

bun add @nbit/bun

# -- or --

npm install @nbit/node

# -- or --

npm install @nbit/cfw # for Cloudflare workers

More Examples

Route params
const routes = defineRoutes((app) => [
  app.post('/users/:id', (request) => {
    const userId = request.params.id; // <-- fully typed
    return { yourUserId: userId };
  }),
]);
Sending a file
const routes = defineRoutes((app) => [
  app.post('/', (request) => {
    return Response.file('assets/index.html');
  }),
]);
Request Headers
const routes = defineRoutes((app) => [
  app.post('/foo', async (request) => {
    const authToken = request.headers.get('Authorization') ?? '';
    // ... check auth ...
    return { success: true };
  }),
]);
Request body
const routes = defineRoutes((app) => [
  app.post('/auth', async (request) => {
    const { username, password } = await request.json();
    const isValid = await checkLogin(username, password);
    if (!isValid) {
      // Send a JSON response _with_ a custom status code
      return Response.json({ success: false }, { status: 403 });
    }
    // ...
  }),
]);
Throwing
const routes = defineRoutes((app) => [
  app.post('/auth', async (request) => {
    const { username, password } = await request.json();
    // The following check with _throw_ if not valid; see below
    await checkLogin(username, password);
    return { success: true };
  }),
]);

async function checkLogin(username, password) {
  if (username !== 'foo' || password !== '123') {
    throw new HttpError({ status: 403, message: 'Unauthorized' });
  }
}

Some of those examples might seem a bit boilerplatey for a hello world, but there's some important ergonomic design decisions with the defineRoutes() and attachRoutes() paradigm, as well as the fact that each route handler takes exactly one input (request) and returns exactly one result.

// TODO: Add more examples

Everything from middleware (which we call context) to body parsers to request params is fully typed, out of the box, using TypeScript's powerful type inference so you don't need to write type annotations everywhere.

const routes = defineRoutes((app) => [
  app.get('/users/:username', async (request) => {
    const username = request.params.username.toLowerCase(); // <-- ✅ TypeScript knows this is a string
    const foo = request.params.foo; // <-- 🚫 Type Error: foo does not exist on params
    const body = await request.json(); // <-- 🚫 Type Error: GET request doesn't have a body
    if (!isValidUsername(username)) {
      throw new HttpError({ status: 403 }); // <-- Throw from any level deep
    }
    const user = await db.getUserByUsername(username);
    if (!user) {
      return; // <-- Will proceed to the next handler, or send 404 if there are no more handlers
    }
    return user; // <<-- Automatically converted to JSON
  }),
]);

Note that in the above code we're defining an array of route handlers.

const routes = defineRoutes((app) => [ ... ]);

This reflects an important principle of this framework; we don't mutate a shared app object. We declaratively define a set of route handlers using defineRoutes(). The result is simply an array (which can be passed into attachRoutes() or used for tests).

From within a route handler you can return new Response(string) or return new Response(stream) or you can use a helper like return Response.json({...}) or return just a plain object and it will be sent as JSON.

Response follows the web standard for the most part, but there's an extra Response.file(path) to serve a static file. We might add additional non-standard helpers in future as well.

You can import { Response } from '@nbit/node' or from @nbit/bun, etc.

In Bun and Cloudflare workers (which have a built-in Response), the Response object is a sub-class of the built-in Response.

Similarly the request object that is passed in to each route handler follows the web standard for the most part, but it has an additional .params for route params as well as whatever custom context methods you define (see below).

Splitting routes into multiple files

This approach to declarative route handlers scales nicely if we want to split our routes into different files as our application grows:

// routes/foo.ts
export default defineRoutes((app) => [
  app.get('/', async (request) => { ... }),
  app.post('/foo', async (request) => { ... }),
]);

// routes/bar.ts (note the optional destructuring below)
export default defineRoutes(({ get, post }) => [
  get('/', async (request) => { ... }),
  post('/bar', async (request) => { ... }),
]);

// routes/index.ts
export { default as foo } from './foo';
export { default as bar } from './bar';

// server.ts
import * as handlers from './routes';

Bun.serve({
  port: 3000,
  fetch: attachRoutes(...Object.values(handlers)),
});

See full examples for Bun, Node or Express.

Context (aka middleware)

The design choice for extensibility is influenced by the way Apollo Server does things; this allows us to maximize type safety while still providing an ergonomic experience for developers.

Essentially you create a context object which will be passed to each route handler. This context object can have helpers for authentication, body parsing, etc.

Example:

import { createApplication, HttpError } from '@nbit/bun';

const { defineRoutes, attachRoutes } = createApplication({
  getContext: (request) => ({
    // We can provide async functions here that can be easily called within our route handlers
    authenticate: async () => { ... },
    someOtherHelper: () => {
      // We can throw a special HttpError from here
      if (!request.headers.get('foo')) {
        throw new HttpError({ status: 403 });
      }
    },
  }),
});

const routes = defineRoutes((app) => [
  app.get('/users/me', (request) => {
    const user = await request.authenticate(); // <-- This is fully typed; TS knows this method is available on `request` because we defined it above
    const foo = request.foo(); // <-- 🚫 Type Error: foo() does not exist on request or context
    return user.someDetails; // <-- We can be sure
  }),
]);

export { defineRoutes, attachRoutes };

Note in the above that whatever we return as part of context gets merged onto the request object. This has been convenient, but I'm not sure if it's too magical, so there's a possibility this might change to request.context in a future version.

Importantly, the context methods, e.g. .authenticate() can throw a special HttpError (or any error really, but HttpError will ensure the right response status, vs a generic error will result in a 500). This ensures we can do something like const { userId } = await request.authenticate() from within a route handler since it will always result in a valid user.

Testing route handlers

Testing a route handler is as easy as constructing a mock request, receiving a response, and asserting the response is as expected. See a live example here.

import { createApplication, Request } from '@nbit/node';

const { defineRoutes, createRequestHandler } = createApplication();

const routes = defineRoutes((app) => [
  app.get('/', (request) => {
    return { hello: 'world' };
  }),
]);

const requestHandler = createRequestHandler(routes);

it('should handle a request', async () => {
  const request = new Request('/');
  const response = await requestHandler(request);
  expect(response.status).toBe(200);
  const data = await response.json();
  expect(data).toEqual({ hello: 'world' });
});

Benchmarks

// TODO

Project Status

It is still early days, and some parts of the API will likely change (I will follow semver and a breaking change will be a major version bump). This is adapted from an internal set of tooling that has been used in production, but that doesn't mean this is stable. Please do try it out and leave feedback, criticisms and thoughts on the design choices and implementation. I know there's still a number of missing pieces, missing examples and missing documentation (file uploads, cors helpers, etc) but I wanted to get this out to gather early feedback. I hope you find this useful, even if merely as something fun to poke around with!

You might also like...

Shifty is a tiny zero-dependency secrets generator, built for the web using TypeScript.

Shifty is a tiny zero-dependency secrets generator, built for the web using TypeScript. Installation yarn add @deepsource/shifty Usage Shifty is built

Nov 24, 2022

Google-Drive-Directory-Index | Combining the power of Cloudflare Workers and Google Drive API will allow you to index your Google Drive files on the browser.

🍿 Google-Drive-Directory-Index Combining the power of Cloudflare Workers and Google Drive will allow you to index your Google Drive files on the brow

Jan 2, 2023

Abusing Cloudflare Workers to establish persistence and exfiltrate sensitive data at the edge.

Abusing Cloudflare Workers This repository contains companion code for the blog post MITM at the Edge: Abusing Cloudflare Workers. malicious-worker/ c

Sep 16, 2022

A Cloudflare Workers service that fetches and renders Notion pages as HTML, Markdown, or JSON.

notion-fetch A Cloudflare Workers service that fetches and renders Notion pages as HTML, Markdown, or JSON. Powered by Durable Objects and R2. Usage P

Jan 6, 2023

A flexible gateway for running ML inference jobs through cloud providers or your own GPU. Powered by Replicate and Cloudflare Workers.

Cogflare (Working title) Cogflare is a Cloudflare Workers application that aims to simplify running distributed ML inference jobs through a central AP

Dec 12, 2022

基于 gh-proxy + Jsdelivr+ cnpmjs + cloudflare workers 的 GitHub Serverless API 工具。

better-github-api Better, Eazy, Access Anywhere 介绍 基于 gh-proxy + Jsdelivr + cnpmjs + cloudflare workers 的 GitHub Serverless API 工具。 cdn.js:仅含 gh-proxy

Nov 23, 2022

A URL shortener that runs on Cloudflare Workers

ITP Works A URL shortener that runs on Cloudflare Workers. It stores the rules in Cloudflare KV storage and sends a 301 redirect when a matched pathna

Mar 4, 2022

Starting template for building a Remix site with CloudFlare Workers (ES Modules Syntax)

Starting template for building a Remix site with CloudFlare Workers (ES Modules Syntax)

May 20, 2022
Owner
Simon Sturmer
Software Engineer, founder, speaker, trainer.
Simon Sturmer
Fast, Bun-powered, and Bun-only(for now) Web API framework with full Typescript support.

Zarf Fast, Bun-powered, and Bun-only(for now) Web API framework with full Typescript support. Quickstart Starting with Zarf is as simple as instantiat

Zarf Framework 65 Dec 28, 2022
Bun-Bakery is a web framework for Bun. It uses a file based router in style like svelte-kit. No need to define routes during runtime.

Bun Bakery Bun-Bakery is a web framework for Bun. It uses a file based router in style like svelte-kit. No need to define routes during runtime. Quick

Dennis Dudek 44 Dec 6, 2022
Functional-style Cloudflare Durable Objects with direct API calls from Cloudflare Workers and TypeScript support.

durable-apis Simplifies usage of Cloudflare Durable Objects, allowing a functional programming style or class style, lightweight object definitions, a

Dabble 12 Jan 2, 2023
Small, typed, dependency free tool to round corners of 2d-polygon provided by an array of { x, y } points.

round-polygon Small, typed, dependency-free tool to round corners of 2d-polygon provided by an array of { x, y } points. The algorithm prevents roundi

Sergey Borovikov 10 Nov 26, 2022
Grupprojekt för kurserna 'Javascript med Ramverk' och 'Agil Utveckling'

JavaScript-med-Ramverk-Laboration-3 Grupprojektet för kurserna Javascript med Ramverk och Agil Utveckling. Utvecklingsguide För information om hur utv

Svante Jonsson IT-Högskolan 3 May 18, 2022
Hemsida för personer i Sverige som kan och vill erbjuda boende till människor på flykt

Getting Started with Create React App This project was bootstrapped with Create React App. Available Scripts In the project directory, you can run: np

null 4 May 3, 2022
Kurs-repo för kursen Webbserver och Databaser

Webbserver och databaser This repository is meant for CME students to access exercises and codealongs that happen throughout the course. I hope you wi

null 14 Jan 3, 2023
A simple, strictly typed ORM, to assist you in using Cloudflare's D1 product

D1-Orm ✨ A simple, strictly typed ORM, to assist you in using Cloudflare's D1 product API reference can be found at https://d1-orm.pages.dev/modules D

null 78 Dec 25, 2022
Lightweight universal Cloudflare API client library for Node.js, Browser, and CF Workers

Cloudflare API Client Lightweight universal HTTP client for Cloudflare API based on Fetch API that works in Node.js, browser, and CF Workers environme

Kriasoft 15 Nov 13, 2022
A collection of useful tools for building web apps on Cloudflare Workers.

Keywork is a batteries-included, magic-free, library for building web apps on Cloudflare Workers. Features ?? Written in TypeScript ?? Modules Support

Nirrius Studio 28 Dec 22, 2022