playbook/outfitter-agents/plugins/outfitter/skills/bun-dev/references/server-patterns.md

410 lines
8.9 KiB
Markdown

# Bun Server Patterns
HTTP, WebSocket, and streaming patterns with Bun.serve.
## Basic HTTP Server
```typescript
Bun.serve({
port: 3000,
hostname: '0.0.0.0', // Listen on all interfaces
fetch(req) {
const url = new URL(req.url);
switch (url.pathname) {
case '/':
return new Response('Hello, World!');
case '/json':
return Response.json({ ok: true });
case '/html':
return new Response('<h1>Hello</h1>', {
headers: { 'Content-Type': 'text/html' }
});
default:
return new Response('Not Found', { status: 404 });
}
},
error(err) {
console.error('Server error:', err);
return new Response(`Error: ${err.message}`, { status: 500 });
}
});
```
## Request Handling
```typescript
Bun.serve({
async fetch(req) {
const url = new URL(req.url);
// Method routing
if (req.method === 'POST' && url.pathname === '/users') {
const body = await req.json();
// Process body...
return Response.json({ id: '123', ...body }, { status: 201 });
}
// Query parameters
if (url.pathname === '/search') {
const query = url.searchParams.get('q');
const page = parseInt(url.searchParams.get('page') || '1');
// Search logic...
}
// Headers
const auth = req.headers.get('Authorization');
const contentType = req.headers.get('Content-Type');
// URL parameters (manual parsing)
const match = url.pathname.match(/^\/users\/([^/]+)$/);
if (match) {
const userId = match[1];
// Fetch user...
}
return new Response('Not Found', { status: 404 });
}
});
```
## Response Patterns
```typescript
// Plain text
new Response('Hello')
// JSON
Response.json({ data: 'value' })
// With status
new Response('Created', { status: 201 })
Response.json({ error: 'Not found' }, { status: 404 })
// With headers
new Response('data', {
headers: {
'Content-Type': 'text/plain',
'Cache-Control': 'max-age=3600',
'X-Custom-Header': 'value'
}
})
// Redirect
Response.redirect('/new-location', 302)
// Stream
new Response(readableStream, {
headers: { 'Content-Type': 'application/octet-stream' }
})
```
## File Serving
```typescript
Bun.serve({
async fetch(req) {
const url = new URL(req.url);
// Serve static files
if (url.pathname.startsWith('/static/')) {
const filepath = `./public${url.pathname}`;
const file = Bun.file(filepath);
if (!(await file.exists())) {
return new Response('Not Found', { status: 404 });
}
return new Response(file.stream(), {
headers: {
'Content-Type': file.type,
'Content-Length': file.size.toString(),
'Cache-Control': 'public, max-age=31536000'
}
});
}
// File download
if (url.pathname.startsWith('/download/')) {
const filename = url.pathname.split('/').pop();
const file = Bun.file(`./files/${filename}`);
return new Response(file.stream(), {
headers: {
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename="${filename}"`
}
});
}
}
});
```
## WebSocket Server
```typescript
type WebSocketData = {
id: string;
userId: string;
joinedAt: Date;
};
const clients = new Map<string, ServerWebSocket<WebSocketData>>();
Bun.serve<WebSocketData>({
port: 3000,
fetch(req, server) {
const url = new URL(req.url);
if (url.pathname === '/ws') {
const userId = url.searchParams.get('userId');
if (!userId) {
return new Response('userId required', { status: 400 });
}
const success = server.upgrade(req, {
data: {
id: crypto.randomUUID(),
userId,
joinedAt: new Date()
}
});
return success ? undefined : new Response('Upgrade failed', { status: 500 });
}
return new Response('Hello');
},
websocket: {
open(ws) {
clients.set(ws.data.id, ws);
ws.subscribe('broadcast');
ws.send(JSON.stringify({ type: 'connected', id: ws.data.id }));
},
message(ws, message) {
const data = JSON.parse(message.toString());
switch (data.type) {
case 'broadcast':
ws.publish('broadcast', JSON.stringify({
from: ws.data.userId,
message: data.message
}));
break;
case 'direct':
const target = clients.get(data.targetId);
target?.send(JSON.stringify({
from: ws.data.userId,
message: data.message
}));
break;
}
},
close(ws) {
clients.delete(ws.data.id);
ws.unsubscribe('broadcast');
}
}
});
```
## Streaming Responses
```typescript
// Server-Sent Events
Bun.serve({
fetch(req) {
const url = new URL(req.url);
if (url.pathname === '/events') {
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
const interval = setInterval(() => {
const event = `data: ${JSON.stringify({ time: Date.now() })}\n\n`;
controller.enqueue(encoder.encode(event));
}, 1000);
// Cleanup on close
req.signal.addEventListener('abort', () => {
clearInterval(interval);
controller.close();
});
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
});
}
}
});
// Chunked transfer
async function* generateChunks() {
for (let i = 0; i < 10; i++) {
yield `Chunk ${i}\n`;
await Bun.sleep(100);
}
}
const response = new Response(
new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
for await (const chunk of generateChunks()) {
controller.enqueue(encoder.encode(chunk));
}
controller.close();
}
})
);
```
## Middleware Pattern
```typescript
type Handler = (req: Request) => Response | Promise<Response>;
type Middleware = (req: Request, next: Handler) => Response | Promise<Response>;
function compose(...middlewares: Middleware[]): Handler {
return (req) => {
let index = 0;
const next: Handler = (req) => {
if (index >= middlewares.length) {
return new Response('Not Found', { status: 404 });
}
const middleware = middlewares[index++];
return middleware(req, next);
};
return next(req);
};
}
// Logging middleware
const logging: Middleware = async (req, next) => {
const start = Bun.nanoseconds();
const response = await next(req);
const duration = (Bun.nanoseconds() - start) / 1_000_000;
console.log(`${req.method} ${new URL(req.url).pathname} - ${duration.toFixed(2)}ms`);
return response;
};
// Auth middleware
const auth: Middleware = async (req, next) => {
const token = req.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
// Validate token...
return next(req);
};
// CORS middleware
const cors: Middleware = async (req, next) => {
if (req.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
});
}
const response = await next(req);
response.headers.set('Access-Control-Allow-Origin', '*');
return response;
};
const handler = compose(cors, logging, auth);
Bun.serve({
fetch: handler
});
```
## Graceful Shutdown
```typescript
const server = Bun.serve({
port: 3000,
fetch(req) {
return new Response('Hello');
}
});
process.on('SIGTERM', () => {
console.log('Shutting down...');
server.stop();
process.exit(0);
});
process.on('SIGINT', () => {
console.log('Interrupted, shutting down...');
server.stop();
process.exit(0);
});
```
## Compression
```typescript
import { gzipSync, gunzipSync, deflateSync, inflateSync } from 'bun';
// Gzip compression
const data = 'Large data string...'.repeat(1000);
const compressed = gzipSync(data);
const decompressed = gunzipSync(compressed);
// Deflate
const deflated = deflateSync('data');
const inflated = inflateSync(deflated);
// Gzip HTTP response
app.get('/large-data', (c) => {
const data = generateLargeDataset();
const json = JSON.stringify(data);
const acceptEncoding = c.req.header('accept-encoding') || '';
if (acceptEncoding.includes('gzip')) {
return c.body(gzipSync(json), {
headers: {
'Content-Type': 'application/json',
'Content-Encoding': 'gzip'
}
});
}
return c.json(data);
});
```
## TLS/HTTPS
```typescript
Bun.serve({
port: 443,
tls: {
key: Bun.file('./key.pem'),
cert: Bun.file('./cert.pem'),
},
fetch(req) {
return new Response('Secure!');
}
});
```