--- 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 ` ``` Becomes something like this: ```html title="index.html" icon="file-code" Home
``` ## React Integration To use React in your client-side code, import `react-dom/client` and render your app. ```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(); ```` ```html title="public/dashboard.html" icon="file-code" Dashboard
``` ```tsx title="src/app.tsx" icon="/icons/typescript.svg" import { useState } from "react"; export default function App() { const [count, setCount] = useState(0); return (

Dashboard

); } ```
## 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" ``` 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" ``` ### 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. 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. ## 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) ``` 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. See the [HTML & static sites documentation](/bundler/html-static#inline-environment-variables) for more details on build-time configuration and examples. ## How It Works Bun uses `HTMLRewriter` to scan for ` ``` - Processes CSS imports and `` tags - Concatenates CSS files - Rewrites url and asset paths to include content-addressable hashes in URLs ```html title="index.html" icon="file-code" ``` - 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 - Combines all ` ``` ```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(); ``` ```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([]); 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 (

User Management

setName(e.target.value)} required /> setEmail(e.target.value)} required />

Users ({users.length})

{users.map(user => (
{user.name}
{user.email}
))}
); } ``` ```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 { 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 This is a work in progress. Features and APIs may change as Bun continues to evolve.