Authentication built in
Add OAuth (GitHub, Google, Microsoft, Discord, Facebook, Apple) with a single option.
Sessions, signed cookies and CSRF-protected flows are handled for you, and
the signed-in user is available on ctx.user.
A modern JavaScript server with the essentials built in: routing, authentication, uploads, WebSockets and testing.
npm install @server/nextimport server from "@server/next";
import { Books } from "./db.js";
export default server()
.get("/books", () => Books.list())
.post("/books", ctx => Books.create(ctx.body))
.delete("/books/:id", ctx => Books.remove(ctx.url.params.id));
Routing, uploads, validation, websockets, auth and many more.
Full OpenAPI schema, validation with Zod, valibot, or arktype, and fully typed.
The same code runs on Node, Bun, Cloudflare, Netlify and more.
Testing helpers built for speed, with no mocking or network calls needed.
Extensive documentation, examples and tutorials for you and your LLM.
Add OAuth (GitHub, Google, Microsoft, Discord, Facebook, Apple) with a single option.
Sessions, signed cookies and CSRF-protected flows are handled for you, and
the signed-in user is available on ctx.user.
import server from "@server/next";
// Sign in at /auth/login/github. No database, no store,
// no callbacks: the profile is signed into the cookie.
export default server({ auth: "cookie:github" })
.get("/me", ctx => ctx.user || 401);
Files stream straight to storage as they arrive, never held in memory, so even large uploads stay fast and cheap.
Point uploads at a local folder or an S3-compatible bucket,
and each stored file shows up on ctx.body.
export default server({ uploads: "./uploads" })
.post("/avatar", ctx => {
console.log(ctx.body.avatar);
// {
// name: "me.png",
// path: "/home/.../uploads/f3a9c2e1.png",
// type: "image/png",
// size: 345465,
// }
});
On Bun you can return JSX from a route and it is sent as HTML. It is
opt-in by updating your tsconfig.json.
This is the primary recommended templating engine, but it is NOT React, just JSX. Interpolation is escaped and safe from XSS.
Read more →export default server()
.get("/", () => (
<html lang="en">
<body>
<h1>Hello {name}</h1>
</body>
</html>
));
No integration needed, return HTML fragments from your routes and let HTMX swap them in.
It blends especially well with JSX, whose escaped interpolation keeps those fragments safe from XSS.
Read more →import server, { file } from "@server/next";
export default server()
.get("/", () => file("index.html"))
.post("/clicked", () => (
<button hx-post="/clicked">Clicked!</button>
));
Route parameters are read straight from the URL and typed for you, so mistakes are caught as you type.
Add a format like :id(number) and the value arrives as a
number, and mark optional segments with ?.
server()
// ctx.url.params.name is a string
.get("/user/:name", ...)
// ctx.url.params.id is a number
.get("/user/:id(number)", ...)
// ctx.url.params.cId is optional
.get("/books/:id/comments/:cId?", ...);
Add size and type limits right on the uploads option, and
oversized or wrong-type files are rejected before your handler runs.
Each stored file gets a random, unguessable id, so nothing is overwritten or predictable.
Read more →export default server({
uploads: {
bucket: "./uploads",
maxFileSize: "5mb",
fileType: ["image/jpeg", "image/png"],
},
}).post("/avatar", ctx => ctx.body.avatar);
Return a fetch() response straight from a route and it is
forwarded as-is: status, headers and a streamed body.
That is all you need for a backend-for-frontend, an API gateway, or hiding an upstream key from the browser.
Read more →export default server()
.get("/gh/*", ctx =>
fetch(`https://api.github.com${ctx.url.pathname.slice(3)}`)
);
Handle WebSocket events with the same routing API. Reply to a single
client with ctx.socket, or broadcast to every connection with
ctx.sockets.
server()
.get("/", () => file("./index.html"))
.socket("message", ctx => {
ctx.socket.send(`You said: ${ctx.body}`);
ctx.sockets.forEach(s => s.send(ctx.body));
});
Return a string, object, status code, file or stream from any route handler.
Server.js infers the response, content type and headers, so handlers just return data.
Read more →import server, { file } from "@server/next";
server()
.get("/text", () => "Hello")
.get("/json", () => ({ ok: true }))
.get("/gone", () => 404)
.get("/file", () => file("./a.pdf"));
Call .test() on any server for an in-process client that runs
requests through the full middleware stack, with no ports, mocking or
network.
It mirrors every HTTP method and sends JSON, FormData or
streams as the body. Each call returns a standard Response,
so you assert on the same status, headers and
.json() a real client would get.
const api = server()
.get("/hello", () => "Hi!")
.post("/echo", ctx => ctx.body)
.test();
const res = await api.post("/echo", { name: "Francisco" });
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ name: "Francisco" });