Attributes
Includes Deno configuration
Repository
Current version released
a month ago
Dependencies
Deso
A simple & fast web framework for Deno
import { Deso } from "jsr:@deso/core";
const app = new Deso();
app.get("/", (ctx) => ctx.text("Hello World"));
app.get("/hello/:name", (ctx) => ctx.text(`Hello ${ctx.param("name")}!`));
await app.serve({ port: 3000 });Installation
deno add jsr:@deso/coreGetting Started
1. Basic Server
import { Deso } from "jsr:@deso/core";
const app = new Deso();
app.get("/", (ctx) => ctx.text("home"));
app.get("/ping", () => new Response("pong"));
await app.serve({ port: 3000 });2. Path Parameters
app.get("/users/:id", (ctx) => ctx.json({ id: ctx.param("id") }));3. Middleware
import type { DesoMiddleware } from "jsr:@deso/core";
const logger: DesoMiddleware = async (ctx, next) => {
const start = Date.now();
const res = await next();
console.log(`${ctx.req().method} ${res.status} ${Date.now() - start}ms`);
return res;
};
app.use(logger);4. Request Body
app.post("/data", async (ctx) => {
const body = await ctx.body("json");
return ctx.json({ received: body });
});Features
- Standard
next()middleware pattern (like Hono, Oak, Express) - Trie-based router with LRU cache — fast matching
- Path params with optional regex constraints
- Nested route groups with shared middleware
- Zod schema validation (
zValidator) - OpenAPI 3.1 spec generation
- WebSocket support with room broadcasting
- Static file serving with Cache-Control
- Rate limiting (sliding window)
- CORS, request ID, and logger middleware
- Type-safe route params (
ParamKeys<Path>) - Error boundaries — handlers & middleware throwing errors return 500
- AsyncLocalStorage for per-request context
Routes
app.get("/", (ctx) => ctx.text("GET /"));
app.post("/", (ctx) => ctx.text("POST /"));
app.put("/", (ctx) => ctx.text("PUT /"));
app.patch("/:id", (ctx) => ctx.text(`PATCH ${ctx.param("id")}`));
app.delete("/:id", (ctx) => ctx.text(`DELETE ${ctx.param("id")}`));
app.head("/", (ctx) => ctx.text(""));
app.options("/", (ctx) => ctx.text(""));
app.any("/hello", (ctx) => ctx.text("any method"));Path Parameters
app.get("/users/:id", (ctx) => ctx.json({ id: ctx.param("id") }));
// Regex constraint
app.get("/posts/:id(\\d+)", (ctx) => ctx.json({ postId: ctx.param("id") }));
// Multiple params
app.get(
"/:tenant/items/:id",
(ctx) => ctx.json({ tenant: ctx.param("tenant"), id: ctx.param("id") }),
);Middleware
Middleware uses the next() pattern. Call next() to continue the chain, or
return a Response to short-circuit.
import type { DesoMiddleware } from "jsr:@deso/core";
const logger: DesoMiddleware = async (ctx, next) => {
const { method } = ctx.req();
const url = ctx.req().url;
const start = Date.now();
const res = await next();
console.log(`${method} ${url} ${res.status} ${Date.now() - start}ms`);
return res;
};
app.use(logger);Middleware Order Matters
Middleware executes in FIFO order for next() calls and LIFO order for next()
returns (like Express/Hono):
app.use(async (_ctx, next) => {
console.log("1-before");
const res = await next();
console.log("1-after");
return res;
});
app.use(async (_ctx, next) => {
console.log("2-before");
const res = await next();
console.log("2-after");
return res;
});
// GET / → logs: 1-before → 2-before → handler → 2-after → 1-afterRoute-level
app.get("/admin", authMiddleware, rateLimiter, (ctx) => ctx.text("admin"));Global
app.use(requestId("x-request-id"));
app.use(cors({ origin: "https://example.com" }));
app.use(logger({ writer: Deno.stdout }));Group
app.group("/api", authMiddleware, () => {
app.get("/users", listUsers);
app.post("/users", createUser);
});Combining Multiple Middlewares
app.post(
"/api/users",
requestId("x-request-id"), // 1. assign request ID
rateLimiter({ windowMs: 60000, max: 30 }), // 2. rate limit
authMiddleware, // 3. authenticate
zValidator("json", UserSchema), // 4. validate body
async (ctx) => { // 5. handler
const user = ctx.get("json");
return ctx.json(user, 201);
},
);Error Handling Middleware
app.use(async (ctx, next) => {
try {
return await next();
} catch (err) {
console.error("Unhandled error", err);
return ctx.oops("internal error", 500);
}
});Context
app.get("/hello", (ctx) => {
ctx.header("X-Custom", "value"); // set response header
const h = ctx.header("Authorization"); // read request header
ctx.set("key", "value"); // store per-request data
const v = ctx.get("key"); // retrieve per-request data
return ctx.json({ message: "hi" });
});Response helpers
ctx.json({ data: "hello" }); // JSON response
ctx.text("hello"); // plain text
ctx.html("<h1>hello</h1>"); // HTML
ctx.stream(readableStream); // streaming response
ctx.oops("not found", 404); // error response
ctx.oops(new Error("bad request")); // error from Error instanceRequest body
app.post("/data", async (ctx) => {
const json = await ctx.body("json"); // parse JSON body
const text = await ctx.body("text"); // raw text
const form = await ctx.body("form"); // FormData
return ctx.json({ received: json });
});Route Groups
app.group("/api/v1", () => {
app.get("/users", listUsers);
app.post("/users", createUser);
});
// Nested
app.group("/api", () => {
app.group("/v1", () => app.get("/items", getItems));
app.group("/v2", () => app.get("/items", getItemsV2));
});Zod Validation
import { z } from "zod";
import { zValidator } from "jsr:@deso/core";
const UserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().min(18).optional(),
});
app.post(
"/users",
zValidator("json", UserSchema),
(ctx) => {
const user = ctx.get("json") as z.infer<typeof UserSchema>;
return ctx.json(user, 201);
},
);
// Query params
app.get(
"/items",
zValidator("query", z.object({ page: z.coerce.number().int().positive() })),
(ctx) => ctx.json(ctx.get("query")),
);
// Route params
app.get(
"/users/:id",
zValidator("params", z.object({ id: z.string().regex(/^\d+$/) })),
(ctx) => ctx.json(ctx.get("params")),
);OpenAPI
import { OpenAPIRegistry } from "jsr:@deso/core";
const registry = new OpenAPIRegistry({
info: { title: "My API", version: "1.0.0" },
servers: [{ url: "https://api.example.com" }],
});
registry.register("GET", "/users", {
summary: "List users",
tags: ["Users"],
parameters: [{ name: "page", in: "query", schema: z.number() }],
responses: {
"200": {
description: "OK",
content: { "application/json": { schema: z.array(UserSchema) } },
},
},
});
console.log(JSON.stringify(registry.generate(), null, 2));WebSocket
app.ws("/chat", {
open(ws) {
console.log("connected");
},
message(ws, event) {
ws.send(`echo: ${event.data}`);
},
close(ws, event) {
console.log("disconnected");
},
error(ws, event) {
console.error("ws error", event);
},
});With middleware
app.ws("/chat", authMiddleware, {
open(ws, ctx) {
room.add(ws);
},
message(ws, event) {
room.broadcast(event.data);
},
});Room broadcasting
import { wsManager } from "jsr:@deso/core";
const room = wsManager.room("chat:general");
app.ws("/chat", {
open(ws) {
room.add(ws);
},
message(ws, event) {
room.broadcast(event.data);
},
});Static Files
import { staticMiddleware } from "jsr:@deso/core";
app.get(
"/*",
staticMiddleware({
assetPath: "./public",
cacheControl: "public, max-age=3600",
}),
);Rate Limiting
import { rateLimiter } from "jsr:@deso/core";
app.use(rateLimiter({ windowMs: 60000, max: 100 }));
app.post(
"/auth/login",
rateLimiter({ windowMs: 60000, max: 5, message: "Too many" }),
loginHandler,
);CORS & Logging
import { cors, logger, requestId } from "jsr:@deso/core";
app.use(requestId("x-request-id"));
app.use(cors({ origin: "https://example.com", methods: ["GET", "POST"] }));
app.use(logger({ writer: Deno.stdout }));Error Handling
Errors thrown in handlers or middleware are caught and return 500. Errors with a
.status property use that status code.
app.get("/crash", () => {
throw new Error("boom");
}); // 500
app.get("/custom", () => {
const err = new Error("not found") as Error & { status: number };
err.status = 404;
throw err; // 404
});
// Catch errors from next()
app.use(async (ctx, next) => {
try {
return await next();
} catch (err) {
return ctx.oops(err, 500);
}
});Server
await app.serve({ port: 3000 });
// With AbortSignal (testing)
const ac = new AbortController();
await app.serve({
port: 0,
signal: ac.signal,
onListen: (addr) => {
console.log(`Listening on ${addr.port}`);
},
});
// AsyncLocalStorage
const app = new Deso({ enableAsyncLocalStorage: true });
app.get("/als", (ctx) => ctx.text(app.als?.get("key") ?? "none"));Benchmarks
deno task benchResults (100k iterations on M-series Mac). Throughput varies by hardware.
| Scenario | Ops/sec |
|---|---|
| Raw Deno.serve handler | 10,789,523 |
| Deso static route | 2,273,194 |
| Deso param route | 1,798,678 |
| Deso global middleware | 1,756,533 |
Full API reference at API.md.
Testing
deno task test # Run all tests
deno task check # Format + lint + test