UNPKG

bun-types

Version:

Type definitions and documentation for Bun, an incredibly fast JavaScript runtime

1,087 lines (870 loc) 26.5 kB
--- title: "Fullstack dev server" description: "Build fullstack applications with Bun's integrated dev server that bundles frontend assets and handles API routes" --- To get started, import HTML files and pass them to the `routes` option in `Bun.serve()`. ```ts title="app.ts" icon="/icons/typescript.svg" import { serve } from "bun"; import dashboard from "./dashboard.html"; import homepage from "./index.html"; const server = serve({ routes: { // ** HTML imports ** // Bundle & route index.html to "/". This uses HTMLRewriter to scan // the HTML for `<script>` and `<link>` tags, runs Bun's JavaScript // & CSS bundler on them, transpiles any TypeScript, JSX, and TSX, // downlevels CSS with Bun's CSS parser and serves the result. "/": homepage, // Bundle & route dashboard.html to "/dashboard" "/dashboard": dashboard, // ** API endpoints ** (Bun v1.2.3+ required) "/api/users": { async GET(req) { const users = await sql`SELECT * FROM users`; return Response.json(users); }, async POST(req) { const { name, email } = await req.json(); const [user] = await sql`INSERT INTO users (name, email) VALUES (${name}, ${email})`; return Response.json(user); }, }, "/api/users/:id": async req => { const { id } = req.params; const [user] = await sql`SELECT * FROM users WHERE id = ${id}`; return Response.json(user); }, }, // Enable development mode for: // - Detailed error messages // - Hot reloading (Bun v1.2.3+ required) development: true, }); console.log(`Listening on ${server.url}`); ``` ```bash terminal icon="terminal" bun run app.ts ``` ## HTML Routes ### HTML Imports as Routes The web starts with HTML, and so does Bun's fullstack dev server. To specify entrypoints to your frontend, import HTML files into your JavaScript/TypeScript/TSX/JSX files. ```ts title="app.ts" icon="/icons/typescript.svg" import dashboard from "./dashboard.html"; import homepage from "./index.html"; ``` These HTML files are used as routes in Bun's dev server you can pass to `Bun.serve()`. ```ts title="app.ts" icon="/icons/typescript.svg" Bun.serve({ routes: { "/": homepage, "/dashboard": dashboard, }, fetch(req) { // ... api requests }, }); ``` When you make a request to `/dashboard` or `/`, Bun automatically bundles the `<script>` and `<link>` tags in the HTML files, exposes them as static routes, and serves the result. ### HTML Processing Example An `index.html` file like this: ```html title="index.html" icon="file-code" <!DOCTYPE html> <html> <head> <title>Home</title> <link rel="stylesheet" href="./reset.css" /> <link rel="stylesheet" href="./styles.css" /> </head> <body> <div id="root"></div> <script type="module" src="./sentry-and-preloads.ts"></script> <script type="module" src="./my-app.tsx"></script> </body> </html> ``` Becomes something like this: ```html title="index.html" icon="file-code" <!DOCTYPE html> <html> <head> <title>Home</title> <link rel="stylesheet" href="/index-[hash].css" /> </head> <body> <div id="root"></div> <script type="module" src="/index-[hash].js"></script> </body> </html> ``` ## React Integration To use React in your client-side code, import `react-dom/client` and render your app. <CodeGroup> ```ts title="src/backend.ts" icon="/icons/typescript.svg" import dashboard from "../public/dashboard.html"; import { serve } from "bun"; serve({ routes: { "/": dashboard, }, async fetch(req) { // ...api requests return new Response("hello world"); }, }); ```` ```tsx title="src/frontend.tsx" icon="/icons/typescript.svg" import { createRoot } from 'react-dom/client'; import App from './app'; const container = document.getElementById('root'); const root = createRoot(container!); root.render(<App />); ```` ```html title="public/dashboard.html" icon="file-code" <!DOCTYPE html> <html> <head> <title>Dashboard</title> <link rel="stylesheet" href="../src/styles.css" /> </head> <body> <div id="root"></div> <script type="module" src="../src/frontend.tsx"></script> </body> </html> ``` ```tsx title="src/app.tsx" icon="/icons/typescript.svg" import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); return ( <div> <h1>Dashboard</h1> <button onClick={() => setCount(count + 1)}>Count: {count}</button> </div> ); } ``` </CodeGroup> ## Development Mode When building locally, enable development mode by setting `development: true` in `Bun.serve()`. ```ts title="src/backend.ts" icon="/icons/typescript.svg" import homepage from "./index.html"; import dashboard from "./dashboard.html"; Bun.serve({ routes: { "/": homepage, "/dashboard": dashboard, }, development: true, fetch(req) { // ... api requests }, }); ``` ### Development Mode Features When `development` is `true`, Bun will: - Include the SourceMap header in the response so that devtools can show the original source code - Disable minification - Re-bundle assets on each request to a `.html` file - Enable hot module reloading (unless `hmr: false` is set) - Echo console logs from browser to terminal ### Advanced Development Configuration `Bun.serve()` supports echoing console logs from the browser to the terminal. To enable this, pass `console: true` in the development object in `Bun.serve()`. ```ts title="src/backend.ts" icon="/icons/typescript.svg" import homepage from "./index.html"; Bun.serve({ // development can also be an object. development: { // Enable Hot Module Reloading hmr: true, // Echo console logs from the browser to the terminal console: true, }, routes: { "/": homepage, }, }); ``` When `console: true` is set, Bun will stream console logs from the browser to the terminal. This reuses the existing WebSocket connection from HMR to send the logs. ### Development vs Production | Feature | Development | Production | | ------------------- | --------------------- | ----------- | | **Source maps** | ✅ Enabled | ❌ Disabled | | **Minification** | ❌ Disabled | ✅ Enabled | | **Hot reloading** | ✅ Enabled | ❌ Disabled | | **Asset bundling** | 🔄 On each request | 💾 Cached | | **Console logging** | 🖥️ Browser → Terminal | ❌ Disabled | | **Error details** | 📝 Detailed | 🔒 Minimal | ## Production Mode Hot reloading and `development: true` helps you iterate quickly, but in production, your server should be as fast as possible and have as few external dependencies as possible. ### Ahead of Time Bundling (Recommended) As of Bun v1.2.17, you can use `Bun.build` or `bun build` to bundle your full-stack application ahead of time. ```bash terminal icon="terminal" bun build --target=bun --production --outdir=dist ./src/index.ts ``` When Bun's bundler sees an HTML import from server-side code, it will bundle the referenced JavaScript/TypeScript/TSX/JSX and CSS files into a manifest object that `Bun.serve()` can use to serve the assets. ```ts title="src/backend.ts" icon="/icons/typescript.svg" import { serve } from "bun"; import index from "./index.html"; serve({ routes: { "/": index }, }); ``` ### Runtime Bundling When adding a build step is too complicated, you can set `development: false` in `Bun.serve()`. This will: - Enable in-memory caching of bundled assets. Bun will bundle assets lazily on the first request to an `.html` file, and cache the result in memory until the server restarts. - Enable `Cache-Control` headers and `ETag` headers - Minify JavaScript/TypeScript/TSX/JSX files ```ts title="src/backend.ts" icon="/icons/typescript.svg" import { serve } from "bun"; import homepage from "./index.html"; serve({ routes: { "/": homepage, }, // Production mode development: false, }); ``` ## API Routes ### HTTP Method Handlers Define API endpoints with HTTP method handlers: ```ts title="src/backend.ts" icon="/icons/typescript.svg" import { serve } from "bun"; serve({ routes: { "/api/users": { async GET(req) { // Handle GET requests const users = await getUsers(); return Response.json(users); }, async POST(req) { // Handle POST requests const userData = await req.json(); const user = await createUser(userData); return Response.json(user, { status: 201 }); }, async PUT(req) { // Handle PUT requests const userData = await req.json(); const user = await updateUser(userData); return Response.json(user); }, async DELETE(req) { // Handle DELETE requests await deleteUser(req.params.id); return new Response(null, { status: 204 }); }, }, }, }); ``` ### Dynamic Routes Use URL parameters in your routes: ```ts title="src/backend.ts" icon="/icons/typescript.svg" serve({ routes: { // Single parameter "/api/users/:id": async req => { const { id } = req.params; const user = await getUserById(id); return Response.json(user); }, // Multiple parameters "/api/users/:userId/posts/:postId": async req => { const { userId, postId } = req.params; const post = await getPostByUser(userId, postId); return Response.json(post); }, // Wildcard routes "/api/files/*": async req => { const filePath = req.params["*"]; const file = await getFile(filePath); return new Response(file); }, }, }); ``` ### Request Handling ```ts title="src/backend.ts" icon="/icons/typescript.svg" serve({ routes: { "/api/data": { async POST(req) { // Parse JSON body const body = await req.json(); // Access headers const auth = req.headers.get("Authorization"); // Access URL parameters const { id } = req.params; // Access query parameters const url = new URL(req.url); const page = url.searchParams.get("page") || "1"; // Return response return Response.json({ message: "Data processed", page: parseInt(page), authenticated: !!auth, }); }, }, }, }); ``` ## Plugins Bun's bundler plugins are also supported when bundling static routes. To configure plugins for `Bun.serve`, add a `plugins` array in the `[serve.static]` section of your `bunfig.toml`. ### TailwindCSS Plugin You can use TailwindCSS by installing and adding the `tailwindcss` package and `bun-plugin-tailwind` plugin. ```bash terminal icon="terminal" bun add tailwindcss bun-plugin-tailwind ``` ```toml title="bunfig.toml" icon="settings" [serve.static] plugins = ["bun-plugin-tailwind"] ``` This will allow you to use TailwindCSS utility classes in your HTML and CSS files. All you need to do is import `tailwindcss` somewhere: ```html title="index.html" icon="file-code" <!doctype html> <html> <head> <link rel="stylesheet" href="tailwindcss" /> <!-- [!code ++] --> </head> <!-- the rest of your HTML... --> </html> ``` Alternatively, you can import TailwindCSS in your CSS file: ```css title="style.css" icon="file-code" @import "tailwindcss"; .custom-class { @apply bg-red-500 text-white; } ``` ```html index.html icon="file-code" <!doctype html> <html> <head> <link rel="stylesheet" href="./style.css" /> <!-- [!code ++] --> </head> <!-- the rest of your HTML... --> </html> ``` ### Custom Plugins Any JS file or module which exports a valid bundler plugin object (essentially an object with a `name` and `setup` field) can be placed inside the plugins array: ```toml title="bunfig.toml" icon="settings" [serve.static] plugins = ["./my-plugin-implementation.ts"] ``` ```ts title="my-plugin-implementation.ts" icon="/icons/typescript.svg" import type { BunPlugin } from "bun"; const myPlugin: BunPlugin = { name: "my-custom-plugin", setup(build) { // Plugin implementation build.onLoad({ filter: /\.custom$/ }, async args => { const text = await Bun.file(args.path).text(); return { contents: `export default ${JSON.stringify(text)};`, loader: "js", }; }); }, }; export default myPlugin; ``` Bun will lazily resolve and load each plugin and use them to bundle your routes. <Note> This is currently in `bunfig.toml` to make it possible to know statically which plugins are in use when we eventually integrate this with the `bun build` CLI. These plugins work in `Bun.build()`'s JS API, but are not yet supported in the CLI. </Note> ## Inline Environment Variables Bun can replace `process.env.*` references in your frontend JavaScript and TypeScript with their actual values at build time. Configure the `env` option in your `bunfig.toml`: ```toml title="bunfig.toml" icon="settings" [serve.static] env = "PUBLIC_*" # only inline env vars starting with PUBLIC_ (recommended) # env = "inline" # inline all environment variables # env = "disable" # disable env var replacement (default) ``` <Note> This only works with literal `process.env.FOO` references, not `import.meta.env` or indirect access like `const env = process.env; env.FOO`. If an environment variable is not set, you may see runtime errors like `ReferenceError: process is not defined` in the browser. </Note> See the [HTML & static sites documentation](/docs/bundler/html-static#inline-environment-variables) for more details on build-time configuration and examples. ## How It Works Bun uses `HTMLRewriter` to scan for `<script>` and `<link>` tags in HTML files, uses them as entrypoints for Bun's bundler, generates an optimized bundle for the JavaScript/TypeScript/TSX/JSX and CSS files, and serves the result. ### Processing Pipeline <Steps> <Step title="1. <script> Processing"> - Transpiles TypeScript, JSX, and TSX in `<script>` tags - Bundles imported dependencies - Generates sourcemaps for debugging - Minifies when `development` is not `true` in `Bun.serve()` ```html title="index.html" icon="file-code" <script type="module" src="./counter.tsx"></script> ``` </Step> <Step title="2. <link> Processing"> - Processes CSS imports and `<link>` tags - Concatenates CSS files - Rewrites url and asset paths to include content-addressable hashes in URLs ```html title="index.html" icon="file-code" <link rel="stylesheet" href="./styles.css" /> ``` </Step> <Step title="3. <img> & Asset Processing"> - Links to assets are rewritten to include content-addressable hashes in URLs - Small assets in CSS files are inlined into `data:` URLs, reducing the total number of HTTP requests sent over the wire </Step> <Step title="4. HTML Rewriting"> - Combines all `<script>` tags into a single `<script>` tag with a content-addressable hash in the URL - Combines all `<link>` tags into a single `<link>` tag with a content-addressable hash in the URL - Outputs a new HTML file </Step> <Step title="5. Serving"> - All the output files from the bundler are exposed as static routes, using the same mechanism internally as when you pass a Response object to `static` in `Bun.serve()`. - This works similarly to how `Bun.build` processes HTML files. </Step> </Steps> ## Complete Example Here's a complete fullstack application example: ```ts title="server.ts" icon="/icons/typescript.svg" import { serve } from "bun"; import { Database } from "bun:sqlite"; import homepage from "./public/index.html"; import dashboard from "./public/dashboard.html"; // Initialize database const db = new Database("app.db"); db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); const server = serve({ routes: { // Frontend routes "/": homepage, "/dashboard": dashboard, // API routes "/api/users": { async GET() { const users = db.query("SELECT * FROM users").all(); return Response.json(users); }, async POST(req) { const { name, email } = await req.json(); try { const result = db.query("INSERT INTO users (name, email) VALUES (?, ?) RETURNING *").get(name, email); return Response.json(result, { status: 201 }); } catch (error) { return Response.json({ error: "Email already exists" }, { status: 400 }); } }, }, "/api/users/:id": { async GET(req) { const { id } = req.params; const user = db.query("SELECT * FROM users WHERE id = ?").get(id); if (!user) { return Response.json({ error: "User not found" }, { status: 404 }); } return Response.json(user); }, async DELETE(req) { const { id } = req.params; const result = db.query("DELETE FROM users WHERE id = ?").run(id); if (result.changes === 0) { return Response.json({ error: "User not found" }, { status: 404 }); } return new Response(null, { status: 204 }); }, }, // Health check endpoint "/api/health": { GET() { return Response.json({ status: "ok", timestamp: new Date().toISOString(), }); }, }, }, // Enable development mode development: { hmr: true, console: true, }, // Fallback for unmatched routes fetch(req) { return new Response("Not Found", { status: 404 }); }, }); console.log(`🚀 Server running on ${server.url}`); ``` ```html title="public/index.html" icon="file-code" <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Fullstack Bun App</title> <link rel="stylesheet" href="../src/styles.css" /> </head> <body> <div id="root"></div> <script type="module" src="../src/main.tsx"></script> </body> </html> ``` ```tsx title="src/main.tsx" import { createRoot } from "react-dom/client"; import { App } from "./App"; const container = document.getElementById("root")!; const root = createRoot(container); root.render(<App />); ``` ```tsx title="src/App.tsx" import { useState, useEffect } from "react"; interface User { id: number; name: string; email: string; created_at: string; } export function App() { const [users, setUsers] = useState<User[]>([]); const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [loading, setLoading] = useState(false); const fetchUsers = async () => { const response = await fetch("/api/users"); const data = await response.json(); setUsers(data); }; const createUser = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); try { const response = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, email }), }); if (response.ok) { setName(""); setEmail(""); await fetchUsers(); } else { const error = await response.json(); alert(error.error); } } catch (error) { alert("Failed to create user"); } finally { setLoading(false); } }; const deleteUser = async (id: number) => { if (!confirm("Are you sure?")) return; try { const response = await fetch(`/api/users/${id}`, { method: "DELETE", }); if (response.ok) { await fetchUsers(); } } catch (error) { alert("Failed to delete user"); } }; useEffect(() => { fetchUsers(); }, []); return ( <div className="container"> <h1>User Management</h1> <form onSubmit={createUser} className="form"> <input type="text" placeholder="Name" value={name} onChange={e => setName(e.target.value)} required /> <input type="email" placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} required /> <button type="submit" disabled={loading}> {loading ? "Creating..." : "Create User"} </button> </form> <div className="users"> <h2>Users ({users.length})</h2> {users.map(user => ( <div key={user.id} className="user-card"> <div> <strong>{user.name}</strong> <br /> <span>{user.email}</span> </div> <button onClick={() => deleteUser(user.id)} className="delete-btn"> Delete </button> </div> ))} </div> </div> ); } ``` ```css title="src/styles.css" icon="file-code" * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: #f5f5f5; color: #333; } .container { max-width: 800px; margin: 0 auto; padding: 2rem; } h1 { color: #2563eb; margin-bottom: 2rem; } .form { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); margin-bottom: 2rem; display: flex; gap: 1rem; flex-wrap: wrap; } .form input { flex: 1; min-width: 200px; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px; } .form button { padding: 0.75rem 1.5rem; background: #2563eb; color: white; border: none; border-radius: 4px; cursor: pointer; } .form button:hover { background: #1d4ed8; } .form button:disabled { opacity: 0.5; cursor: not-allowed; } .users { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .user-card { display: flex; justify-content: space-between; align-items: center; padding: 1rem; border-bottom: 1px solid #eee; } .user-card:last-child { border-bottom: none; } .delete-btn { padding: 0.5rem 1rem; background: #dc2626; color: white; border: none; border-radius: 4px; cursor: pointer; } .delete-btn:hover { background: #b91c1c; } ``` ## Best Practices ### Project Structure ``` my-app/ ├── src/ │ ├── components/ │ │ ├── Header.tsx │ │ └── UserList.tsx │ ├── styles/ │ │ ├── globals.css │ │ └── components.css │ ├── utils/ │ │ └── api.ts │ ├── App.tsx │ └── main.tsx ├── public/ │ ├── index.html │ ├── dashboard.html │ └── favicon.ico ├── server/ │ ├── routes/ │ │ ├── users.ts │ │ └── auth.ts │ ├── db/ │ │ └── schema.sql │ └── index.ts ├── bunfig.toml └── package.json ``` ### Environment-Based Configuration ```ts title="server/config.ts" icon="/icons/typescript.svg" export const config = { development: process.env.NODE_ENV !== "production", port: process.env.PORT || 3000, database: { url: process.env.DATABASE_URL || "./dev.db", }, cors: { origin: process.env.CORS_ORIGIN || "*", }, }; ``` ### Error Handling ```ts title="server/middleware.ts" icon="/icons/typescript.svg" export function errorHandler(error: Error, req: Request) { console.error("Server error:", error); if (process.env.NODE_ENV === "production") { return Response.json({ error: "Internal server error" }, { status: 500 }); } return Response.json( { error: error.message, stack: error.stack, }, { status: 500 }, ); } ``` ### API Response Helpers ```ts title="server/utils.ts" icon="/icons/typescript.svg" export function json(data: any, status = 200) { return Response.json(data, { status }); } export function error(message: string, status = 400) { return Response.json({ error: message }, { status }); } export function notFound(message = "Not found") { return error(message, 404); } export function unauthorized(message = "Unauthorized") { return error(message, 401); } ``` ### Type Safety ```ts title="types/api.ts" icon="/icons/typescript.svg" export interface User { id: number; name: string; email: string; created_at: string; } export interface CreateUserRequest { name: string; email: string; } export interface ApiResponse<T> { data?: T; error?: string; } ``` ## Deployment ### Production Build ```bash terminal icon="terminal" # Build for production bun build --target=bun --production --outdir=dist ./server/index.ts # Run production server NODE_ENV=production bun dist/index.js ``` ### Docker Deployment ```dockerfile title="Dockerfile" icon="docker" FROM oven/bun:1 as base WORKDIR /usr/src/app # Install dependencies COPY package.json bun.lockb ./ RUN bun install --frozen-lockfile # Copy source code COPY . . # Build application RUN bun build --target=bun --production --outdir=dist ./server/index.ts # Production stage FROM oven/bun:1-slim WORKDIR /usr/src/app COPY --from=base /usr/src/app/dist ./ COPY --from=base /usr/src/app/public ./public EXPOSE 3000 CMD ["bun", "index.js"] ``` ### Environment Variables ```ini title=".env.production" icon="file-code" NODE_ENV=production PORT=3000 DATABASE_URL=postgresql://user:pass@localhost:5432/myapp CORS_ORIGIN=https://myapp.com ``` ## Migration from Other Frameworks ### From Express + Webpack ```ts title="server.ts" icon="/icons/typescript.svg" // Before (Express + Webpack) app.use(express.static("dist")); app.get("/api/users", (req, res) => { res.json(users); }); // After (Bun fullstack) serve({ routes: { "/": homepage, // Replaces express.static "/api/users": { GET() { return Response.json(users); }, }, }, }); ``` ### From Next.js API Routes ```ts title="server.ts" icon="/icons/typescript.svg" // Before (Next.js) export default function handler(req, res) { if (req.method === 'GET') { res.json(users); } } // After (Bun) "/api/users": { GET() { return Response.json(users); } } ``` ## Limitations and Future Plans ### Current Limitations - `bun build` CLI integration is not yet available for fullstack apps - Auto-discovery of API routes is not implemented - Server-side rendering (SSR) is not built-in ### Planned Features - Integration with `bun build` CLI - File-based routing for API endpoints - Built-in SSR support - Enhanced plugin ecosystem <Note>This is a work in progress. Features and APIs may change as Bun continues to evolve.</Note>