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

8.9 KiB

Bun Server Patterns

HTTP, WebSocket, and streaming patterns with Bun.serve.

Basic HTTP Server

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

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

// 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

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

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

// 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

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

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

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

Bun.serve({
  port: 443,
  tls: {
    key: Bun.file('./key.pem'),
    cert: Bun.file('./cert.pem'),
  },
  fetch(req) {
    return new Response('Secure!');
  }
});