410 lines
8.9 KiB
Markdown
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!');
|
|
}
|
|
});
|
|
```
|