运行时包管理器打包器测试运行器代码检查格式化指南参考博客反馈

路由

在 Bun.serve 中使用静态路径、参数和通配符定义路由

使用 routes 属性(静态路径、参数和通配符)向 Bun.serve() 添加路由,或使用 fetch 方法处理未匹配的请求。

Bun.serve() 的路由器基于 uWebSocket 的树形结构方案构建。路由器还添加了 SIMD 加速的路由参数解码和 JavaScriptCore 结构缓存,以充分发挥现代硬件的性能潜力。

基本设置

server.ts
Bun.serve({
  routes: {
    "/": () => new Response("首页"),
    "/api": () => Response.json({ success: true }),
    "/users": async () => Response.json({ users: [] }),
  },
  fetch() {
    return new Response("未匹配路由");
  },
});

Bun.serve() 中的路由会接收一个 BunRequest(继承自 Request),并返回一个 Response 或 Promise<Response>。由于路由使用这些 Request 和 Response 类型,因此发送和接收 HTTP 请求时可以更轻松地使用相同的代码。

TypeScript
// 简化示例
interface BunRequest<T extends string> extends Request {
  params: Record<T, string>;
  readonly cookies: CookieMap;
}

异步路由

异步/等待

在路由处理函数中使用 async/await,以返回一个 Promise<Response>。

TypeScript
import { sql, serve } from "bun";

serve({
  port: 3001,
  routes: {
    "/api/version": async () => {
      const [version] = await sql`SELECT version()`;
      return Response.json(version);
    },
  },
});

Promise

你也可以直接从路由处理函数返回一个 Promise<Response>。

TypeScript
import { sql, serve } from "bun";

serve({
  routes: {
    "/api/version": () => {
      return new Promise(resolve => {
        setTimeout(async () => {
          const [version] = await sql`SELECT version()`;
          resolve(Response.json(version));
        }, 100);
      });
    },
  },
});

路由优先级

Bun 按具体程度从高到低匹配路由:

  1. 精确路由(/users/all)
  2. 参数路由(/users/:id)
  3. 通配符路由(/users/*)
  4. 全局捕获(/*)
TypeScript
Bun.serve({
  routes: {
    // 越具体的路由优先匹配
    "/api/users/me": () => new Response("当前用户"),
    "/api/users/:id": req => new Response(`用户 ${req.params.id}`),
    "/api/*": () => new Response("API 捕获全部"),
    "/*": () => new Response("全局捕获全部"),
  },
});

类型安全的路由参数

TypeScript 会解析以字符串字面量形式传入的路由参数,因此访问 request.params 时,编辑器会显示自动补全。

index.ts
import type { BunRequest } from "bun";

Bun.serve({
  routes: {
    // 当以字符串字面量传入时,TypeScript 知道 params 的结构
    "/orgs/:orgId/repos/:repoId": req => {
      const { orgId, repoId } = req.params;
      return Response.json({ orgId, repoId });
    },

    "/orgs/:orgId/repos/:repoId/settings": (
      // 可选:可以显式为 BunRequest 传入类型参数:
      req: BunRequest<"/orgs/:orgId/repos/:repoId/settings">,
    ) => {
      const { orgId, repoId } = req.params;
      return Response.json({ orgId, repoId });
    },
  },
});

Bun 会自动解码百分号编码的路由参数值,包括 Unicode 字符。对于无效的 Unicode,Bun 会将其替换为 Unicode 替换字符(\uFFFD)。

静态响应

路由也可以是 Response 对象(无需处理函数)。Bun.serve() 会针对零分配调度对其进行优化,非常适合健康检查、重定向和固定内容:

TypeScript
Bun.serve({
  routes: {
    // 健康检查
    "/health": new Response("OK"),
    "/ready": new Response("准备就绪", {
      headers: {
        // 传递自定义头部
        "X-Ready": "1",
      },
    }),

    // 重定向
    "/blog": Response.redirect("https://bun.sh/blog"),

    // API 响应
    "/api/config": Response.json({
      version: "1.0.0",
      env: "production",
    }),
  },
});

静态响应在初始化后不分配额外内存。相比手动返回 Response 对象,通常能提升至少 15% 的性能。

Bun 会在服务器对象的整个生命周期内缓存静态路由响应。要重新加载静态路由,请调用 server.reload(options)。

文件响应与静态响应的区别

从路由提供文件时,具体行为取决于你是将文件内容缓冲起来,还是直接提供文件:

TypeScript
Bun.serve({
  routes: {
    // 静态路由 — 内容在启动时缓冲到内存
    "/logo.png": new Response(await Bun.file("./logo.png").bytes()),

    // 文件路由 — 每次请求从文件系统读取内容
    "/download.zip": new Response(Bun.file("./download.zip")),
  },
});

静态路由 (new Response(await file.bytes())) 会在启动时将内容缓冲至内存:

  • 请求时无文件系统 I/O — 内容完全从内存服务
  • 支持 ETag — 自动生成与验证缓存的 ETag
  • 支持 If-None-Match — 客户端 ETag 匹配时返回 304 Not Modified
  • 无 404 处理 — 缺失文件在启动时报错,不在运行时 404
  • 内存占用 — 文件内容全量存储于 RAM
  • 适合用于:小型静态资源、API 响应、频繁访问的文件

文件路由 (new Response(Bun.file(path))) 每次请求从文件系统读取:

  • 每次请求都会执行文件系统读取,检查文件存在并读取内容
  • 内置 404 处理 — 若文件不存在或变得不可访问返回 404 Not Found
  • 支持 Last-Modified — 基于文件修改时间处理 If-Modified-Since 头
  • 支持 If-Modified-Since — 缓存有效时返回 304 Not Modified
  • 支持 范围请求 — 自动处理部分内容请求,返回 Content-Range 头
  • 支持流传输 — 使用带背压处理的缓冲读取器,实现高效内存使用
  • 内存高效 — 只缓冲传输时的小块数据,不缓存整个文件
  • 适合用于:大文件、动态内容、用户上传、频繁变更的文件

目录路由

若要在某个 URL 前缀下提供整个目录树,请将 { dir } 作为路由值传入。路由路径必须以 /* 结尾。

TypeScript
Bun.serve({
  routes: {
    "/static/*": { dir: "./public" },
  },
});

Bun 会对请求 URL 中前缀之后的部分进行一次百分号解码,并相对于 dir 打开该路径。对于非规范路径,Bun 会返回 404,因此实际提供的路径始终是路由器所匹配的路径。如果路径中包含 .、..、空片段、%2F,或将可直接出现在路径片段中的字符编码为 %XX 序列,则该路径为非规范路径。在 Linux 上,打开文件时会使用 openat2(RESOLVE_IN_ROOT),因此内核会限制符号链接,防止其指向 dir 之外。

目录路由与文件路由共享响应处理路径:

  • Content-Type 根据文件扩展名设置。
  • 每个响应都会发送 Last-Modified 和弱 ETag(W/"<size>-<mtime>"),并会根据 If-Modified-Since / If-None-Match 返回 304 Not Modified。
  • 支持 范围请求,并使用 Accept-Ranges: bytes 和 Content-Range。
  • 请求解析到的目录若没有尾随 /,则会收到重定向到尾随斜杠 URL 的 301 响应。若 URL 有尾随斜杠,Bun 会从该目录提供 index.html。
  • 缺失文件会返回 404。

传入 statCache: false 可禁用按路径缓存的 Last-Modified(每个路由可节省约 20 KB)。


流式传输文件

要流式传输文件,返回一个以 BunFile 对象作为 body 的 Response。

TypeScript
Bun.serve({
  fetch(req) {
    return new Response(Bun.file("./hello.txt"));
  },
});

要发送文件的一部分,请在 Bun.file 对象上使用 slice(start, end) 方法。Bun 会自动在 Response 对象上设置 Content-Range 和 Content-Length 标头。

TypeScript
Bun.serve({
  fetch(req) {
    // 解析 `Range` 头部
    const [start = 0, end = Infinity] = req.headers
      .get("Range") // Range: bytes=0-100
      .split("=") // ["Range: bytes", "0-100"]
      .at(-1) // "0-100"
      .split("-") // ["0", "100"]
      .map(Number); // [0, 100]

    // 返回文件的片段
    const bigFile = Bun.file("./big-video.mp4");
    return new Response(bigFile.slice(start, end));
  },
});

fetch 请求处理器

fetch 处理器会处理未匹配任何路由的传入请求。它接收一个 Request 对象,并返回一个 Response 或 Promise<Response>。

TypeScript
Bun.serve({
  fetch(req) {
    const url = new URL(req.url);
    if (url.pathname === "/") return new Response("首页!");
    if (url.pathname === "/blog") return new Response("博客!");
    return new Response("404!");
  },
});

fetch 处理器支持 async/await:

TypeScript
import { sleep, serve } from "bun";

serve({
  async fetch(req) {
    const start = performance.now();
    await sleep(10);
    const end = performance.now();
    return new Response(`睡眠了 ${end - start} 毫秒`);
  },
});

也支持基于 Promise 的响应:

TypeScript
Bun.serve({
  fetch(req) {
    // 将请求转发到另一个服务器
    return fetch("https://example.com");
  },
});

fetch 处理器还会将 Server 对象作为其第二个参数接收。

TypeScript
// `server` 作为第二个参数传递给 `fetch`。
const server = Bun.serve({
  fetch(req, server) {
    const ip = server.requestIP(req);
    return new Response(`您的 IP 是 ${ip.address}`);
  },
});