Documentation
⚠️ WIP This is an experimental library right now!
A fully-fledged web server for Bun and Node.js, with all the basics built-in:
import server from "@server/next";
export default server(options)
.get("/books", () => Book.list())
.post("/books", BookSchema, (ctx) => {
return Book.create(ctx.body).save();
});
It includes all the things you would expect from a modern Server framework, like routing, static file serving, body+file parsing, gzip+brotli, streaming, testing, error handling, websockets, etc.
We also have integrations and adaptors for these:
- KV Stores: in-memory, Redis, Consul, DynamoDB, Level.
- Buckets: AWS S3, Cloudflare R2, Backblaze B2.
- Validation libraries: Zod, Joi, Yup, Validate, etc.
- Auth: JWT, Session, Cookies, Social login.
// index.test.js
// How to test your server with the built-in methods
import app from "./"; // Import your normal app
const api = app.test(); // Very convenient helper, AXIOS-like interface
it("can retrieve the book list", async () => {
const { data: books } = await api.get("/books/");
expect(books[0]).toEqual({ id: 0, name: ... });
});
Getting started
First install it:
npm install @server/next
yarn add @server/next
bun install @server/next
Now you can create your first simple server:
// index.js
import server from "@server/next";
export default server()
.get("/", () => "Hello world")
.post("/", (ctx) => {
console.log(ctx.body);
return 201;
});
Then run node .
or bun .
and open your browser on http://localhost:3000/ to see the message.
There are some major configuration options that you might want to set up though, enumerated in the Basic Usage and explained through the docs.
Guides
Basic usage
Now that you know how to create a barebones server, there are some important bits that you might want to update.
SECRET
: create an .env
file (ignored in git with .gitignore
) with the SECRET=
and then a long, unique random secret. It will be used to sign and/or encrypt things as needed.
store
: almost anything you want to persist will need a KV store to do so. For dev you can use an in-memory or a file-based (easy for debugging!) store, but for production systems you would normally use something like Redis. Can be as easy as const store = new Map();
for dev.
Note:
bucket
is still not available
bucket
: if you want to accept user files you will need a place to persist them. By default they are put in the filesystem, but since most cloud providers are ephemeral, provide a bucket
and it will be used to handle the files directly.
An example of how that works in practice:
// index.js
import server from "@server/next";
import Bucket from "bucket/b2";
import { createClient } from "redis";
const bucket = Bucket("mybucketname", { id, key });
const store = createClient("...").connect();
export default server({ bucket, store })
.get("/", () => "Hello world")
.post("/", (ctx) => {
console.log(ctx.body);
return 201;
});
Middleware
Validation
Stores
File handling
To manage files, you need to install and use the library bucket
, which is a very thin wrapper for file management systems. It is also created by the makers of Server.js.
The easiest and default is to set a folder in your filesystem:
import FileSystem from "bucket/fs";
const uploads = FileSystem("./uploads");
// All paths are relative to the CWD
export default server({ uploads })
.get("/", () => "Hello")
.put("/users/:id", async (ctx) => {
// This is the plain string as the file name, already in our FS
const fileName = ctx.body.profile;
// 'yOuZEdSsNLq8PgZyLhSz0Llh.jpg'
// Convert it into a File instance
const file = ctx.uploads.file(fileName);
// Now we can use other methods if we want
// .info(), .read(), .write(), .pipe(), .pipeTo(), etc
const info = await file.info();
// {
// id: "yOuZEdSsNLq8PgZyLhSz0Llh.jpg",
// type: "jpg",
// size: 435435,
// timestamp: "2024-08-07T14:26:37Z",
// // Note: this can be customized providing the option "domain"
// url: "file:///Users/me/my-project/uploads/yOuZEdSsNLq8PgZyLhSz0Llh.jpg",
// }
return 200;
});
To upload the files to a 3rd party system, you just need to use the corresponding bucket
implementation (or write a thin compatibility layer). Let's see an example with Backblaze's B2:
import server from "@server/next";
import Backblaze from "bucket/b2";
const uploads = Backblaze("bucket-name", {
id: process.env.BACKBLAZE_ID,
key: process.env.BACKBLAZE_KEY,
});
export default server({ uploads })
.put("/users/:id", async (ctx) => {
const fileName = ctx.body.profile;
// 'yOuZEdSsNLq8PgZyLhSz0Llh.jpg'
// Convert it into a File instance
const file = ctx.uploads.file(fileName);
// Now we can use other methods if we want
const info = await file.info();
// {
// id: "yOuZEdSsNLq8PgZyLhSz0Llh.jpg",
// type: "jpg",
// size: 435435,
// timestamp: "2024-08-07T14:26:37Z",
// url: "https://f???.backblazeb2.com/???/yOuZEdSsNLq8PgZyLhSz0Llh.jpg",
// }
return 200;
});
.;
Example: resizing the user profile picture
Let's see a complete example of uploading and resizing a user profile picture:
import server, { status } from "@server/next";
import sharp from "sharp";
import FileSystem from "bucket/fs";
const uploads = FileSystem("./uploads");
// All paths are relative to the CWD
export default server({ uploads })
.get("/", () => "Hello")
.put("/users/:id", async (ctx) => {
// Create the instance of the file to read and write
const src = ctx.uploads.file(ctx.body.profile);
const dst = ctx.uploads.file("/profile/" + ctx.url.params.id + ".jpg");
const ext = src.id.split(".").pop();
if (!["jpg", "jpeg", "png", "webp", "avif"].includes(ext)) {
await src.remove(); // Don't store it
return status(400).json({ error: "Invalid file format" });
}
// Create the Readable, Transform and Writable Node streams
await pipeline(
src.readable("node"),
sharp().resize(200, 200).jpg(),
dst.writable("node")
);
// We no longer need the original file
await src.remove();
return status(200).json({ updated: true });
});
Note that the option uploads
gets converted into a Bucket
instance and passed as ctx.uploads
. Th
Options
Options docs here
Router
Router docs here
Context
Context docs here
Reply
Reply docs here
Runtimes
There are many runtimes where Server works! We put a lot of work to make sure it works the same way with minimal changes in them, this includes:
- Node.js of course, including everywhere that Node is supported (VPS, Heroku, Render, etc).
- Bun, and it will be even faster!
- Cloudflare Worker
- Netlify Function + Edge function
FAQ
How is it different from Hono?
Server.js attempts to run your code unmodified in all runtimes. With Hono you need to change the code for different runtimes:
// Server.js code for Node.js, Bun and Netlify
import server from "@server/next";
export default server().get("/", () => "Hello server!");
// Hono code for Node.js
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Node.js!'))
serve(app)
// Hono code for Bun
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Bun!'))
export default app
// Hono code for Netlify
import { Hono } from 'jsr:@hono/hono'
import { handle } from 'jsr:@hono/hono/netlify'
const app = new Hono()
app.get('/', (c) => c.text('Hello Hono!'))
export default handle(app)