Skip to content

bunpy.serve

from bunpy.serve import serve

def handler(req):
    return f"Hello, {req.query.get('name', 'world')}!"

serve(handler, port=3000)
bunpy server.py
# Listening on http://localhost:3000

bunpy.serve is an HTTP server built directly into the runtime. It handles routing, parsing, and response serialisation in Go – no WSGI, no ASGI, no framework required. For simple services and APIs it is the fastest path from code to running server.

Signature

from bunpy.serve import serve

serve(handler, port=3000, hostname="127.0.0.1", development=False)

Options

OptionTypeDefaultDescription
portint3000TCP port to bind
hostnamestr"127.0.0.1"Bind address. Use "0.0.0.0" to accept external connections.
developmentboolFalseEnable verbose request/response logging
tlsdictNoneTLS config: {"cert": "cert.pem", "key": "key.pem"}
max_request_sizeint10_485_760Maximum body size in bytes (default 10 MB)

Request object

Every call to handler receives a Request object:

AttributeTypeDescription
req.methodstrHTTP method (GET, POST, PUT, DELETE, …)
req.urlstrFull request URL including scheme and host
req.pathstrURL path component (no query string)
req.querydictParsed query parameters (first value per key)
req.headersdictRequest headers (lowercase keys)
req.bodybytesRaw request body
req.json()anyParse body as JSON. Raises ValueError if body is not valid JSON.
req.text()strDecode body as UTF-8
req.form()dictParse application/x-www-form-urlencoded body

Returning a response

The handler can return a string, bytes, dict, or a Response object.

String (plain text, status 200)

def handler(req):
    return "Hello!"

Dict

def handler(req):
    return {
        "status": 200,
        "headers": {"Content-Type": "application/json"},
        "body": '{"ok": true}',
    }
KeyTypeDefaultDescription
statusint200HTTP status code
headersdict{}Response headers
bodystr or bytes""Response body

Response object

from bunpy.serve import serve, Response

def handler(req):
    return Response(
        body='{"ok": true}',
        status=200,
        headers={"Content-Type": "application/json"},
    )

serve(handler, port=3000)

Examples

JSON API

import json
from bunpy.serve import serve

users = [
    {"id": 1, "name": "Alice"},
    {"id": 2, "name": "Bob"},
]

def handler(req):
    if req.path == "/users" and req.method == "GET":
        return {
            "status": 200,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps(users),
        }

    if req.path == "/users" and req.method == "POST":
        data = req.json()
        new_user = {"id": len(users) + 1, "name": data["name"]}
        users.append(new_user)
        return {
            "status": 201,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps(new_user),
        }

    return {"status": 404, "body": "not found"}

serve(handler, port=3000)

Path routing

import json
from bunpy.serve import serve

def handler(req):
    parts = req.path.strip("/").split("/")

    if parts[0] == "health":
        return {"status": 200, "body": "ok"}

    if parts[0] == "users" and len(parts) == 2:
        user_id = int(parts[1])
        return {
            "status": 200,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps({"id": user_id, "name": f"User {user_id}"}),
        }

    return {"status": 404, "body": "not found"}

serve(handler, port=3000)

Middleware pattern

import time
import json
from bunpy.serve import serve

def log_middleware(handler):
    def wrapped(req):
        start = time.monotonic()
        resp = handler(req)
        elapsed = (time.monotonic() - start) * 1000
        print(f"{req.method} {req.path} -> {resp.get('status', 200)} ({elapsed:.1f}ms)")
        return resp
    return wrapped

def auth_middleware(handler):
    def wrapped(req):
        token = req.headers.get("authorization", "")
        if not token.startswith("Bearer "):
            return {"status": 401, "body": "unauthorized"}
        return handler(req)
    return wrapped

def app(req):
    return {
        "status": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({"message": "protected resource"}),
    }

serve(log_middleware(auth_middleware(app)), port=3000)

Serve static files

import os
import mimetypes
from bunpy.serve import serve

STATIC_DIR = "./public"

def handler(req):
    if req.method != "GET":
        return {"status": 405, "body": "method not allowed"}

    # Strip the leading slash and resolve against the static dir
    rel_path = req.path.lstrip("/") or "index.html"
    abs_path = os.path.join(STATIC_DIR, rel_path)

    # Prevent path traversal
    if not os.path.abspath(abs_path).startswith(os.path.abspath(STATIC_DIR)):
        return {"status": 403, "body": "forbidden"}

    if not os.path.isfile(abs_path):
        return {"status": 404, "body": "not found"}

    content_type, _ = mimetypes.guess_type(abs_path)
    with open(abs_path, "rb") as f:
        return {
            "status": 200,
            "headers": {"Content-Type": content_type or "application/octet-stream"},
            "body": f.read(),
        }

serve(handler, port=8080)

HTTPS / TLS

from bunpy.serve import serve

def handler(req):
    return {"status": 200, "body": "secure!"}

serve(handler, port=443, tls={
    "cert": "/etc/ssl/certs/server.pem",
    "key": "/etc/ssl/private/server.key",
})

Async handler

Handlers can be async def. bunpy runs async handlers on the event loop without any extra setup:

import asyncio
import json
from bunpy.serve import serve

async def handler(req):
    # Simulate an async database call
    await asyncio.sleep(0.01)
    resp = await fetch("https://api.example.com/data")
    data = resp.json()
    return {
        "status": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps(data),
    }

serve(handler, port=3000)

Global shortcut

The Bun.serve global is an alias for bunpy.serve.serve:

def handler(req):
    return "Hello!"

Bun.serve({"fetch": handler, "port": 3000})

This form accepts a single dict rather than positional arguments, matching Bun’s API.