Skip to main content

Migrating from 0.9.x

This guide is for apps running burger-api 0.9.x and @burger-api/cli 0.9.x. The 0.9.x line stays on npm (burger-api@0.9.7, @burger-api/cli@0.9.9), so the move is not urgent, but it is a rewrite rather than a patch. Three things changed at the core:

  • Middleware became hooks: six named lifecycle points instead of a middleware chain.
  • BurgerRequest became BurgerContext: one typed context for every hook and handler.
  • Configuration moved into convention files: src/hooks.ts, src/plugins.ts, burger.build.ts.

Plan for an afternoon for a small API, longer if you have custom auth or global middleware. The hook system, validation, and configuration pages explain the new model in full; this page is about getting your old code across.

Upgrade the packages​

bun add burger-api@latest
bun add -g @burger-api/cli@latest

Deploying to plain Node.js? Add the official adapter as well:

bun add @burger-api/node-server

Checklist​

0.9.x1.0.0-betaWhat to do
BurgerRequestBurgerContextRename the import and the parameter. Handlers are export async function GET(ctx: BurgerContext). See Request Context.
Middleware type and the middleware chainHooks (onRequest, transform, beforeRoute, afterRoute, mapResponse, onError)Decide what each middleware was doing: stop, decorate, or transform. Map it to the matching hook. See Hook System.
Route export const middleware in route.tsRoute hooks.tsMove the code to a hooks.ts next to route.ts. beforeRoute replaces a middleware that short-circuits; afterRoute / mapResponse replace one that returns a response transform.
globalMiddleware in new Burger({...})src/hooks.tsExport global hooks from src/hooks.ts (onRequest for work before routing, beforeRoute for work after validation).
CLI burger-api serveburger-api devRename the command and any package script that calls it.
burger.config.tsburger.build.tsRename the file. It is build-time only; dev and start read new Burger({...}). doctor flags the old name as an error.
use.ts / webhook.ts route filesconfig.tsThese convention files were removed. Route options now live in config.ts.
Auth factories in ecosystem/middlewares/ (jwt, apiKey, ...)Plugins in ecosystem/plugins/ (jwtAuth, apiKey, ...)Install with burger-api add jwt-auth and register in src/plugins.ts. See Ecosystem.
Group/folder files shared across routesSelf-contained route directoriesGroups only strip the URL. Copy shared schema.ts / hooks.ts code into each route that needs it, or import a shared module. See File-Based Routing.
req.validated?.query with a manual genericctx.validated.query via defineRoute / defineHooksPer-method schema.ts exports plus defineRoute infer ctx.validated with no generic to write.
Validation errors: 400 with { errors }422 Problem Details (RFC 9457)Update clients and tests. errorFormat: "plain" in validation config gets the old-style body shape back. See Validation Errors.
Body schema skipped non-JSON bodiesBody schema rejects non-JSON with 415Send application/json (or application/*+json), or drop the body schema for form endpoints.
req.validated optionally populatedctx.validated always an object after validationThe slots you declared are typed non-optional; other slots are unknown.
req.params and req.wildcardParams optionalctx.params object, ctx.wildcardParams array, both always presentRemove null checks; {} / [] are the empty values.
Mutable ctx.services per requestOne frozen, shared ctx.servicesAssigning keys now throws. Put per-request values in a transform hook instead.
ctx.set plain objectTracked ctx.setKeep using ctx.set for status and headers; it now tracks changes so untouched responses are returned as-is.
OpenAPI metadata openapi export in route.ts, title in new Burger({...})openapi.ts per-method exports, document metadata in src/openapi.config.tsMove metadata into its own file; new Burger({ title }) still works as a fallback. See OpenAPI.
A handler could return a non-ResponseNon-Response returns fail loud with a 500Always return Response.json(...) or new Response(...).

Step by step​

1. A route handler​

Before (0.9.x):

api/products/route.ts
import type { BurgerRequest } from "burger-api";

export async function GET(req: BurgerRequest) {
const query = new URL(req.url).searchParams;
return Response.json({ search: query.get("search") });
}

After (1.0.0-beta):

src/api/products/route.ts
import type { BurgerContext } from "burger-api";

export async function GET(ctx: BurgerContext) {
return Response.json({ search: ctx.query.search });
}

ctx.query is parsed lazily from the request, so you no longer build a URL by hand. See Request Context for every property.

2. Route middleware becomes route hooks​

Before (0.9.x), middleware in route.ts did three different jobs in one shape:

api/products/route.ts
import type { BurgerRequest, Middleware } from "burger-api";

export const middleware: Middleware[] = [
(req: BurgerRequest) => {
if (req.headers.get("x-admin") !== "true") {
return Response.json({ error: "Forbidden" }, { status: 403 });
}
return async (response) => {
response.headers.set("x-products", "1");
return response;
};
},
];

export async function GET(req: BurgerRequest) {
return Response.json({ items: [] });
}

After (1.0.0-beta), the stop and the transform are separate hook points in hooks.ts:

src/api/products/hooks.ts
import type { RouteHooks } from "burger-api";

export const beforeRoute: RouteHooks["beforeRoute"] = (ctx) => {
if (ctx.headers.get("x-admin") !== "true") {
return Response.json({ error: "Forbidden" }, { status: 403 });
}
};

export const afterRoute: RouteHooks["afterRoute"] = () => (response) => {
response.headers.set("x-products", "1");
return response;
};

Mapping rules:

  • A middleware that returned a Response becomes a beforeRoute hook that returns a Response.
  • A middleware that returned a function (response) => response becomes afterRoute or mapResponse returning that function. mapResponse is the last chance to change headers.
  • A middleware that returned undefined becomes a hook that returns undefined.

onRequest is app-level only. You cannot scope it to one route: declaring it in a route's hooks.ts fails the TypeScript check, and the runtime warns and ignores it. See Route Hooks.

3. Global middleware becomes src/hooks.ts​

Before (0.9.x):

index.ts
import { Burger } from "burger-api";
import { logger } from "./middleware/logger";

const burger = new Burger({
apiDir: "./api",
globalMiddleware: [logger],
});

burger.serve(4000);

After (1.0.0-beta), the entry stays small and the hooks live in a convention file:

src/index.ts
import { Burger } from "burger-api";

const burger = new Burger({
apiDir: "./src/api",
});

burger.serve(4000);
src/hooks.ts
import { logger } from "../ecosystem/hooks/logger/logger";

export const onRequest = [logger()];

The same applies to CORS and rate limiting: install the hook from the ecosystem with burger-api add cors and compose it in src/hooks.ts. See Global Hooks and Ecosystem.

4. Validation schema​

Before (0.9.x), schema and handler were coupled in one file with lowercase method keys:

api/products/route.ts
import { z } from "zod";
import type { BurgerRequest } from "burger-api";

export const schema = {
get: {
query: z.object({ search: z.string() }),
},
post: {
body: z.object({ name: z.string() }),
},
};

export async function GET(
req: BurgerRequest<{ query: z.infer<typeof schema.get.query> }>
) {
return Response.json({ search: req.validated?.query });
}

export async function POST(
req: BurgerRequest<{ body: z.infer<typeof schema.post.body> }>
) {
return Response.json({ name: req.validated?.body?.name });
}

After (1.0.0-beta), the schema is its own file with uppercase per-method exports, and defineRoute infers the types:

src/api/products/schema.ts
import { z } from "zod";

export const GET = { query: z.object({ search: z.string() }) };
export const POST = { body: z.object({ name: z.string() }) };
src/api/products/route.ts
import { defineRoute } from "burger-api";
import { GET as GetSchema, POST as PostSchema } from "./schema";

export const GET = defineRoute(GetSchema, (ctx) => {
const { search } = ctx.validated.query;
return Response.json({ search });
});

export const POST = defineRoute(PostSchema, (ctx) => {
const { name } = ctx.validated.body;
return Response.json({ name });
});

Two behavior changes to watch for:

  • A failed validation is now a 422 Problem Details response, not a 400 with { errors }. See Validation Errors.
  • When a body schema is declared, a non-JSON body (text/plain, form data) is rejected with 415 Unsupported Media Type instead of skipping validation.

The old manual generic (BurgerContext<typeof RouteSchema>) still works when you prefer it. See Schema Definition.

5. Auth middleware becomes a plugin​

Before (0.9.x), auth was middleware added to globalMiddleware:

index.ts
import { Burger } from "burger-api";
import { jwt } from "./ecosystem/middlewares/jwt-auth/jwt-auth";

const burger = new Burger({
apiDir: "./api",
globalMiddleware: [jwt({ secret: process.env.JWT_SECRET! })],
});

burger.serve(4000);

After (1.0.0-beta), auth is a plugin registered in src/plugins.ts, and routes opt in or out in config.ts:

src/plugins.ts
import type { PluginRegistrar } from "burger-api";
import { jwtAuth } from "../ecosystem/plugins/jwt-auth/jwt-auth";

export default (burger: PluginRegistrar) => {
burger.usePlugin(jwtAuth({ secret: process.env.JWT_SECRET }));
};
src/api/public/health/config.ts
export default { auth: false };
src/api/admin/config.ts
export default { auth: { required: true, roles: ["admin"] } };

The plugin attaches the decoded token as ctx.user. API keys work the same way with apiKey({ keys: [...] }). See JWT Auth and API Key Auth.

6. Build config​

Before (0.9.x):

burger.config.ts
export default {
apiDir: "./api",
pageDir: "./pages",
apiPrefix: "/api",
pagePrefix: "/",
debug: false,
};

After (1.0.0-beta):

burger.build.ts
import type { BuildConfig } from "burger-api";

export default {
apiDir: "./src/api",
pageDir: "./src/pages",
apiPrefix: "/api",
pagePrefix: "/",
debug: false,
} satisfies Partial<BuildConfig>;

burger.build.ts is read by burger-api build, inspect, and doctor only. Runtime options live in new Burger({...}), and doctor warns when the two disagree. See Configuration.

7. CLI commands in package.json​

Before (0.9.x), scripts called Bun directly:

package.json
{
"scripts": {
"dev": "bun --watch src/index.ts",
"start": "bun src/index.ts",
"build": "bun build src/index.ts --outdir ./dist"
}
}

After (1.0.0-beta), scripts call the CLI, which the project should carry as a dependency:

package.json
{
"scripts": {
"dev": "burger-api dev",
"start": "burger-api start",
"build": "burger-api build src/index.ts",
"typecheck": "bun check"
},
"devDependencies": {
"@burger-api/cli": "^1.0.0-beta"
}
}

Add --target=<platform> when you build for something other than Bun: burger-api build src/index.ts --target=node. See CLI Tool and Build Command.

8. OpenAPI metadata​

Before (0.9.x), metadata was an openapi export in route.ts and a title in the constructor:

api/products/route.ts
export const openapi = {
get: {
summary: "List products",
tags: ["Products"],
},
};
index.ts
const burger = new Burger({
apiDir: "./api",
title: "My API",
description: "Products and orders",
});

After (1.0.0-beta), route metadata moves to openapi.ts with per-method exports, and document metadata moves to src/openapi.config.ts:

src/api/products/openapi.ts
export const GET = {
summary: "List products",
tags: ["Products"],
};
src/openapi.config.ts
import type { OpenAPIConfig } from "burger-api";

export default {
title: "My API",
description: "Products and orders",
version: "1.0.0",
} satisfies OpenAPIConfig;

The spec is served at /openapi.json and the docs UI at /docs, as before. Swagger UI is still the default; scalarDocs() and redocDocs() are also built in. See OpenAPI.

Verify the upgrade​

Run these from the project root:

burger-api doctor
burger-api inspect
bun run dev
bun run typecheck
bun test
  • doctor checks the project shape and fails on leftover burger.config.ts. See Doctor.
  • inspect lists the config, routes, hooks, and plugins the CLI actually discovers, which catches files the scanner is not loading. See Inspect.
  • bun run dev starts the server with hot reload. Open /docs to confirm the spec still generates.

Getting help​

If something here does not work, or the migration guide is missing a case, open an issue. The 0.9.7 release stays on npm while you work through the upgrade.