
Bun Dev Server
- 37 installs
- 4 repo stars
- Updated January 26, 2026
- daleseo/bun-skills
Sets up high-performance Bun.serve development servers with Hot Module Replacement and React Fast Refresh for frontend, API, or full-stack apps.
About
Configures Bun.serve dev servers with HMR, React Fast Refresh, and live reload for SPA, API, full-stack, or static setups. Developers use it when standing up a local dev server on Bun.
- React Fast Refresh and HMR out of the box
- Supports SPA, API (Hono), full-stack, and static servers
Bun Dev Server by the numbers
- 37 all-time installs (skills.sh)
- Ranked #1,405 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/daleseo/bun-skills --skill bun-dev-serverAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 37 |
|---|---|
| repo stars | ★ 4 |
| Last updated | January 26, 2026 |
| Repository | daleseo/bun-skills ↗ |
What it does
Sets up high-performance Bun.serve development servers with Hot Module Replacement and React Fast Refresh for frontend, API, or full-stack apps.
Files
Bun Development Server Setup
You are assisting with setting up a high-performance development server using Bun.serve with Hot Module Replacement (HMR) and React Fast Refresh.
Workflow
1. Determine Server Type
Ask the user what type of development server they need:
- React/Frontend App: SPA with React Fast Refresh
- API Server: REST/GraphQL API with auto-reload
- Full-Stack App: Frontend + API combined
- Static Server: File server with live reload
2. Check Prerequisites
# Verify Bun installation
bun --version
# Check if project has package.json
ls -la package.jsonIf no package.json exists, suggest running bun init first.
3. Install Dependencies
For React Apps:
bun add react react-dom
bun add -d @types/react @types/react-domFor API with Hono (recommended):
bun add honoFor Full-Stack:
bun add react react-dom hono
bun add -d @types/react @types/react-dom4. Create Server Configuration
React Development Server
Create server.ts in the project root:
import type { ServerWebSocket } from "bun";
const clients = new Set<ServerWebSocket<unknown>>();
const server = Bun.serve({
port: 3000,
async fetch(request, server) {
const url = new URL(request.url);
// WebSocket for HMR
if (url.pathname === "/_hmr") {
const upgraded = server.upgrade(request);
if (upgraded) return undefined;
return new Response("WebSocket upgrade failed", { status: 500 });
}
// Serve index.html for SPA routing
if (url.pathname === "/" || !url.pathname.includes(".")) {
return new Response(
Bun.file("public/index.html"),
{ headers: { "Content-Type": "text/html" } }
);
}
// Serve static files
const filePath = `public${url.pathname}`;
const file = Bun.file(filePath);
if (await file.exists()) {
return new Response(file);
}
return new Response("Not Found", { status: 404 });
},
websocket: {
open(ws) {
clients.add(ws);
console.log("HMR client connected");
},
close(ws) {
clients.delete(ws);
console.log("HMR client disconnected");
},
message(ws, message) {
// Handle client messages if needed
},
},
});
console.log(`🚀 Dev server running at http://localhost:${server.port}`);
// Watch for file changes
const watcher = Bun.file.watch(import.meta.dir + "/src", {
recursive: true,
});
for await (const event of watcher) {
if (event.kind === "change" && event.path.endsWith(".tsx")) {
console.log(`📝 File changed: ${event.path}`);
// Notify all connected clients to reload
for (const client of clients) {
client.send(JSON.stringify({ type: "reload" }));
}
}
}Create public/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bun + React App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
<!-- HMR Client -->
<script>
const ws = new WebSocket('ws://localhost:3000/_hmr');
ws.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.type === 'reload') {
console.log('🔄 Reloading...');
window.location.reload();
}
});
ws.addEventListener('close', () => {
console.log('❌ HMR connection lost. Reconnecting...');
setTimeout(() => window.location.reload(), 1000);
});
</script>
</body>
</html>Create src/index.tsx:
import { render } from 'react-dom';
import App from './App';
const root = document.getElementById('root');
render(<App />, root);Create src/App.tsx:
export default function App() {
return (
<div>
<h1>Welcome to Bun + React!</h1>
<p>Edit src/App.tsx to see HMR in action</p>
</div>
);
}API Server with Hono
Create server.ts:
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
const app = new Hono();
// Middleware
app.use('*', cors());
app.use('*', logger());
// Routes
app.get('/', (c) => {
return c.json({ message: 'Welcome to Bun API' });
});
app.get('/api/health', (c) => {
return c.json({ status: 'ok', timestamp: Date.now() });
});
// Example POST endpoint
app.post('/api/users', async (c) => {
const body = await c.req.json();
return c.json({ created: true, data: body }, 201);
});
// Start server
const server = Bun.serve({
port: process.env.PORT || 3000,
fetch: app.fetch,
});
console.log(`🚀 API server running at http://localhost:${server.port}`);Full-Stack Server
Create server.ts:
import { Hono } from 'hono';
import { serveStatic } from 'hono/bun';
const app = new Hono();
// API routes
const api = new Hono();
api.get('/health', (c) => c.json({ status: 'ok' }));
api.get('/users', (c) => c.json({ users: [] }));
app.route('/api', api);
// Serve static files
app.use('/*', serveStatic({ root: './public' }));
// SPA fallback
app.get('*', (c) => c.html(Bun.file('public/index.html')));
const server = Bun.serve({
port: 3000,
fetch: app.fetch,
});
console.log(`🚀 Full-stack server at http://localhost:${server.port}`);5. Configure React Fast Refresh (Advanced)
For true React Fast Refresh, create hmr-runtime.ts:
// React Fast Refresh runtime
let timeout: Timer | null = null;
export function refresh() {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
// Re-import the App component
import('./App.tsx?t=' + Date.now()).then((module) => {
const { render } = require('react-dom');
const root = document.getElementById('root');
render(module.default(), root);
});
}, 100);
}
// Listen for HMR events
if (import.meta.hot) {
import.meta.hot.accept(() => {
refresh();
});
}6. Environment Configuration
Create .env.development:
# Server
PORT=3000
NODE_ENV=development
# API
API_URL=http://localhost:3000/api
# Features
ENABLE_HMR=trueCreate .env.production:
# Server
PORT=8080
NODE_ENV=production
# API
API_URL=https://api.example.com
# Features
ENABLE_HMR=falseLoad environment in server.ts:
// Environment is loaded automatically by Bun
const isDev = process.env.NODE_ENV === 'development';
const port = process.env.PORT || 3000;7. Update package.json Scripts
Add development scripts:
{
"scripts": {
"dev": "bun run --hot server.ts",
"dev:watch": "bun run --watch server.ts",
"start": "NODE_ENV=production bun run server.ts",
"build": "bun build src/index.tsx --outdir=dist --minify",
"clean": "rm -rf dist"
}
}Script explanations:
dev: Run with hot reload (restarts on file changes)dev:watch: Watch mode (faster, but doesn't reload on crash)start: Production modebuild: Build frontend for production
8. Configure TypeScript
Update tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"types": ["bun-types"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
"noEmit": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*", "server.ts"]
}9. Create Project Structure
Generate complete project structure:
project/
├── server.ts # Development server
├── src/
│ ├── index.tsx # App entry point
│ ├── App.tsx # Main component
│ ├── components/ # React components
│ └── styles/ # CSS files
├── public/
│ ├── index.html # HTML template
│ └── assets/ # Static assets
├── .env.development
├── .env.production
├── package.json
├── tsconfig.json
└── README.md10. Advanced: HTTPS for Local Development
For HTTPS support (needed for some browser APIs):
import { readFileSync } from 'fs';
const server = Bun.serve({
port: 3000,
tls: {
cert: readFileSync('./localhost.pem'),
key: readFileSync('./localhost-key.pem'),
},
fetch: app.fetch,
});
console.log(`🔒 HTTPS server at https://localhost:${server.port}`);Generate certificates with:
# Install mkcert first: brew install mkcert
mkcert -install
mkcert localhost 127.0.0.1 ::111. Proxy Configuration (for existing backends)
If user needs to proxy API requests to another server:
const app = new Hono();
// Proxy /api requests to backend
app.all('/api/*', async (c) => {
const url = new URL(c.req.url);
const backendUrl = `http://localhost:8080${url.pathname}${url.search}`;
const response = await fetch(backendUrl, {
method: c.req.method,
headers: c.req.raw.headers,
body: c.req.method !== 'GET' ? await c.req.raw.text() : undefined,
});
return new Response(response.body, {
status: response.status,
headers: response.headers,
});
});Testing the Setup
After creation, guide user to test:
# 1. Start dev server
bun run dev
# 2. Open browser
open http://localhost:3000
# 3. Make a change to src/App.tsx
# 4. Verify HMR reloads the page
# 5. Test API endpoints
curl http://localhost:3000/api/healthTroubleshooting
HMR not working
// Check if WebSocket connection is established
// Open browser console and look for:
// "HMR client connected"
// If not, verify:
// 1. Port is correct
// 2. No firewall blocking WebSocket
// 3. Server is running with --hot flagPort already in use
# Find process using port 3000
lsof -ti:3000
# Kill the process
kill -9 $(lsof -ti:3000)
# Or use a different port
PORT=3001 bun run devCORS issues
Add CORS headers to server:
app.use('*', cors({
origin: 'http://localhost:3000',
credentials: true,
}));Performance Tips
1. Use --hot for development: Faster than --watch for most cases 2. Minimize file watcher scope: Watch only src/ directory 3. Use HTTP/2: Enable for faster parallel loading 4. Cache static assets: Add Cache-Control headers
app.use('/assets/*', async (c, next) => {
await next();
c.header('Cache-Control', 'public, max-age=31536000');
});Completion Checklist
- ✅ Development server created
- ✅ HMR configured and tested
- ✅ Environment variables set up
- ✅ Package.json scripts added
- ✅ Project structure organized
- ✅ TypeScript configured
- ✅ Browser successfully connects
- ✅ File changes trigger reload
Next Steps
Suggest to the user: 1. Add error boundaries for better error handling 2. Set up ESLint and Prettier 3. Configure path aliases in tsconfig.json 4. Add development vs production builds 5. Consider adding bun-test for testing
HMR Implementation Examples
This document provides detailed examples of Hot Module Replacement (HMR) implementations for different frameworks and use cases with Bun.
Basic WebSocket HMR
The simplest HMR implementation using WebSocket:
// server.ts
import type { ServerWebSocket } from "bun";
const clients = new Set<ServerWebSocket<unknown>>();
const server = Bun.serve({
port: 3000,
fetch(request, server) {
const url = new URL(request.url);
if (url.pathname === "/_hmr") {
server.upgrade(request);
return undefined;
}
return new Response(Bun.file("index.html"));
},
websocket: {
open(ws) {
clients.add(ws);
},
close(ws) {
clients.delete(ws);
},
message() {},
},
});
// File watcher
const watcher = Bun.file.watch("./src");
for await (const event of watcher) {
for (const client of clients) {
client.send(JSON.stringify({ type: "reload", file: event.path }));
}
}<!-- Client-side HMR -->
<script>
const ws = new WebSocket('ws://localhost:3000/_hmr');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'reload') {
console.log(`Reloading due to change in ${data.file}`);
window.location.reload();
}
};
</script>React Fast Refresh
Advanced HMR with React component preservation:
// hmr-client.tsx
interface HMRMessage {
type: 'update' | 'reload';
modules?: string[];
}
class HMRClient {
private ws: WebSocket;
private pendingUpdates = new Set<string>();
constructor(port: number) {
this.ws = new WebSocket(`ws://localhost:${port}/_hmr`);
this.ws.onmessage = this.handleMessage.bind(this);
this.ws.onclose = () => {
console.log('HMR disconnected, reloading...');
setTimeout(() => window.location.reload(), 1000);
};
}
private async handleMessage(event: MessageEvent) {
const data: HMRMessage = JSON.parse(event.data);
if (data.type === 'reload') {
window.location.reload();
return;
}
if (data.type === 'update' && data.modules) {
for (const modulePath of data.modules) {
await this.hotUpdate(modulePath);
}
}
}
private async hotUpdate(modulePath: string) {
// Add cache-busting timestamp
const url = `${modulePath}?t=${Date.now()}`;
try {
// Dynamic import with timestamp
const module = await import(url);
// If it's a React component, trigger re-render
if (module.default?.$$typeof) {
this.refreshReactComponent(modulePath, module.default);
}
} catch (error) {
console.error(`Failed to hot update ${modulePath}:`, error);
window.location.reload();
}
}
private refreshReactComponent(modulePath: string, Component: any) {
// Find all instances of this component in the tree
// and trigger a re-render
// This is a simplified version - real React Fast Refresh
// uses the react-refresh runtime
const event = new CustomEvent('hmr:component-update', {
detail: { modulePath, Component }
});
window.dispatchEvent(event);
}
}
// Initialize HMR client
if (import.meta.env.DEV) {
new HMRClient(3000);
}CSS HMR (No Page Reload)
Update CSS without full page reload:
// server.ts
const cssWatcher = Bun.file.watch("./src/**/*.css");
for await (const event of cssWatcher) {
if (event.kind === "change") {
const cssContent = await Bun.file(event.path).text();
for (const client of clients) {
client.send(JSON.stringify({
type: "css-update",
path: event.path,
content: cssContent
}));
}
}
}// Client-side CSS injection
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'css-update') {
// Find or create style tag for this file
let styleTag = document.querySelector(`style[data-path="${data.path}"]`);
if (!styleTag) {
styleTag = document.createElement('style');
styleTag.setAttribute('data-path', data.path);
document.head.appendChild(styleTag);
}
styleTag.textContent = data.content;
console.log(`✨ Updated CSS: ${data.path}`);
}
};Module-Level HMR
For non-React modules (utilities, stores, etc.):
// store.ts
let state = { count: 0 };
export function getState() {
return state;
}
export function setState(newState: typeof state) {
state = newState;
}
// HMR preservation
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
console.log('Store updated');
// Preserve state across updates
});
import.meta.hot.dispose((data) => {
// Save state before reload
data.state = state;
});
}Smart File Watching
Only reload affected modules:
// dependency-graph.ts
class DependencyGraph {
private graph = new Map<string, Set<string>>();
addDependency(parent: string, child: string) {
if (!this.graph.has(parent)) {
this.graph.set(parent, new Set());
}
this.graph.get(parent)!.add(child);
}
getAffectedModules(changedFile: string): Set<string> {
const affected = new Set<string>();
const queue = [changedFile];
while (queue.length > 0) {
const file = queue.shift()!;
affected.add(file);
// Find all modules that import this file
for (const [parent, children] of this.graph) {
if (children.has(file) && !affected.has(parent)) {
queue.push(parent);
}
}
}
return affected;
}
}
const graph = new DependencyGraph();
// Build graph from imports
const watcher = Bun.file.watch("./src");
for await (const event of watcher) {
const affected = graph.getAffectedModules(event.path);
for (const client of clients) {
client.send(JSON.stringify({
type: "update",
modules: Array.from(affected)
}));
}
}Error Overlay
Display runtime errors in browser:
// error-overlay.ts
export function showErrorOverlay(error: Error) {
const overlay = document.createElement('div');
overlay.id = 'error-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
color: #fff;
padding: 20px;
font-family: monospace;
z-index: 999999;
overflow: auto;
`;
overlay.innerHTML = `
<h1 style="color: #ff5555;">Runtime Error</h1>
<pre style="color: #ffff55;">${error.message}</pre>
<pre style="color: #888;">${error.stack}</pre>
<button onclick="this.parentElement.remove()">Dismiss</button>
`;
document.body.appendChild(overlay);
}
// Listen for errors
window.addEventListener('error', (event) => {
if (import.meta.env.DEV) {
showErrorOverlay(event.error);
}
});
// HMR success - remove overlay
window.addEventListener('hmr:success', () => {
document.getElementById('error-overlay')?.remove();
});Vue 3 HMR
Hot Module Replacement for Vue components:
// hmr-vue.ts
import { createApp } from 'vue';
let app = createApp(App);
app.mount('#app');
if (import.meta.hot) {
import.meta.hot.accept('./App.vue', (newModule) => {
app.unmount();
app = createApp(newModule.default);
app.mount('#app');
});
}Svelte HMR
// hmr-svelte.ts
import App from './App.svelte';
let app = new App({
target: document.getElementById('app')!
});
if (import.meta.hot) {
import.meta.hot.accept();
import.meta.hot.dispose(() => {
app.$destroy();
});
}Server-Side Code Reload
Reload server-side code without dropping connections:
// hot-server.ts
let requestHandler = (await import('./routes.ts')).default;
const server = Bun.serve({
port: 3000,
async fetch(request) {
return requestHandler(request);
},
});
// Watch server code
const serverWatcher = Bun.file.watch("./routes.ts");
for await (const event of serverWatcher) {
console.log('🔄 Reloading server code...');
// Re-import with cache bust
const newModule = await import(`./routes.ts?t=${Date.now()}`);
requestHandler = newModule.default;
console.log('✅ Server code reloaded');
}Production HMR Disable
Ensure HMR is disabled in production:
// config.ts
export const HMR_ENABLED = process.env.NODE_ENV === 'development';
// server.ts
if (HMR_ENABLED) {
setupHMR();
}Performance Optimization
Debounce file changes to avoid excessive reloads:
function debounce<T extends (...args: any[]) => void>(
fn: T,
delay: number
): T {
let timeout: Timer;
return ((...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
}) as T;
}
const notifyClients = debounce((file: string) => {
for (const client of clients) {
client.send(JSON.stringify({ type: "reload", file }));
}
}, 100);