Skip to main content

Server

The Server class is the main entry point for creating Balda applications. It handles HTTP requests, manages middleware, and orchestrates the entire application lifecycle.

Creating a Server​

import { Server } from "balda";

const server = new Server({
port: 3000,
host: "localhost",
});

Server Lifecycle​

1. Initialization​

When you create a server instance, it:

  • Sets up the runtime connector (Node.js, Bun, or Deno)
  • Initializes the router
  • Applies global middleware
  • Sets up the body parser
const server = new Server({
port: 3000,
plugins: {
cors: { origin: "*" },
bodyParser: { json: { sizeLimit: "1mb" } },
},
});

2. Route Registration​

Routes can be registered in two ways:

Direct registration:

server.router.get("/users", (req, res) => {
res.json({ users: [] });
});

Controller-based registration:

@controller("/users")
export class UsersController {
@get("/")
async getUsers(req, res) {
res.json({ users: [] });
}
}

Using the global router:

For library-level or modular route definitions, you can import the shared router and register routes without a Server instance. These routes will be consumed by the server at startup.

import { router } from "balda";

router.get("/health", (req, res) => {
res.json({ status: "ok" });
});

router.post("/login", authMiddleware, (req, res) => {
res.json({ token: "..." });
});

router.get("/admin", [authMiddleware, adminMiddleware], (req, res) => {
res.json({ ok: true });
});

3. Server Startup​

When you call server.listen(), the server:

  • Imports controllers based on patterns
  • Registers all routes
  • Applies plugins
  • Runs beforeStart hooks in order
  • Starts listening for requests
server.listen(({ port, host }) => {
console.log(`Server running on http://${host}:${port}`);
});

// optional you can specify the port directly in listen
server.listen((3000, { port, host }) => {
console.log(`Server running on http://${host}:${port}`);
});

Lifecycle Hooks​

beforeStart​

Register hooks that run after bootstrap (controllers imported, plugins applied) but before the server starts accepting requests. This is useful for connecting to databases, warming caches, or performing other async setup.

server.beforeStart(async () => {
await connectToDatabase();
});

server.beforeStart(async () => {
await warmUpCache();
});

server.listen(({ port }) => {
console.log(`Server running on port ${port}`);
});

Multiple hooks are called sequentially in the order they are registered. If any hook throws, the server will not start and the error is passed to the listen callback.

beforeClose​

Register hooks that run before the server stops accepting requests and before any connection is closed. This is the place to close resources that need a graceful goodbye — for example, notifying connected WebSocket clients before the transport goes down (see WebSockets: Shutting down).

server.beforeClose(async () => {
for (const client of wss.clients) {
client.close(1001, "Server shutting down");
}
});

await server.close();

Multiple hooks run sequentially in registration order. If a hook throws, the remaining hooks still run, the server still closes, and the error is rethrown from close()/disconnect() after the close completes.

Server Configuration​

import { Server } from "balda";

const server = new Server({
port: 3000,
host: "localhost",
controllerPatterns: ["./src/controllers/**/*.ts"],
plugins: {
cors: { origin: "*" },
bodyParser: { json: { sizeLimit: "10mb" } },
},
swagger: true,
});
tip

See the Configuration Guide for detailed options and Plugins Overview for available plugins.

Server Methods​

HTTP Methods​

// Basic routes using server instance
server.router.get("/users", (req, res) => res.json({ users: [] }));
server.router.post("/users", (req, res) => res.created(req.body));
server.router.put("/users/:id", (req, res) => res.json({ id: req.params.id }));
server.router.patch("/users/:id", (req, res) => res.json(req.body));
server.router.delete("/users/:id", (req, res) => res.noContent());

// Basic routes using global router
router.get("/users", (req, res) => res.json({ users: [] }));
router.post("/users", (req, res) => res.created(req.body));
router.put("/users/:id", (req, res) => res.json({ id: req.params.id }));
router.patch("/users/:id", (req, res) => res.json(req.body));
router.delete("/users/:id", (req, res) => res.noContent());

// With middleware
server.router.post("/users", authMiddleware, (req, res) =>
res.created(req.body),
);
router.put("/users/:id", [authMiddleware, auditMiddleware], (req, res) => {
res.json({ id: req.params.id });
});

Middleware Management​

// Single middleware
server.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});

// Multiple middleware
server.use(authMiddleware, loggingMiddleware);

Path Normalization and Groups​

Paths are normalized to avoid duplicate or trailing slashes. Grouping allows composing a base path and shared middleware:

server.router.group("/api", (r) => {
r.get("/status", (req, res) => res.json({ ok: true })); // => /api/status
});

server.router.group("/v1", authMiddleware, (r) => {
r.get("/me", (req, res) => res.json({ me: "..." })); // => /v1/me with auth
});

server.router.group("/admin", [authMiddleware, adminMiddleware], (r) => {
r.get("/dashboard", (req, res) => res.json({ ok: true })); // => /admin/dashboard with both
});

The router supports the following overloads:

  • handler-only: get(path, handler)
  • with options: get(path, options, handler)

Error Handling​

server.setErrorHandler((req, res, next, error) => {
// `error` is the raw thrown value — stack and cause included — whatever
// `exposeErrorDetails` is set to. Sanitize before it reaches the client.
logger.error({ err: error }, "Request failed");

if (error.name === "ValidationError") {
return res.badRequest({ error: error.message });
}

res.internalServerError({ error: "Internal server error" });
});

Custom Not Found Handler​

By default, Balda returns a standardized 404 error response when a route is not found. You can customize this behavior using setNotFoundHandler. The optional third argument is the error that triggered the response — a RouteNotFoundError, or a MethodNotAllowedError when the path exists under another method:

server.setNotFoundHandler((req, res, error) => {
res.status(404).json({
error: "Not Found",
code: error.name,
message: `The route ${req.url} does not exist`,
timestamp: new Date().toISOString(),
});
});

Advanced Examples:

HTML Response for Web Applications:

server.setNotFoundHandler((req, res) => {
res.status(404).html(`
<!DOCTYPE html>
<html>
<head><title>404 - Page Not Found</title></head>
<body>
<h1>Oops! Page not found</h1>
<p>The page you're looking for doesn't exist.</p>
<a href="/">Go back home</a>
</body>
</html>
`);
});

Resetting to Default Behavior:

To reset to the default not found handler, pass undefined:

server.setNotFoundHandler(undefined);

Server Properties​

Read-only Properties​

// Get server URL
console.log(server.url); // http://localhost:3000

// Get server port
console.log(server.port); // 3000

// Get server host
console.log(server.host); // localhost

// Check if server is listening
console.log(server.isListening); // true/false

Utility Methods​

import type { NodeHttpClient } from "balda";

declare module "balda" {
interface Server {
db: typeof db;
}
}

// Get temporary directory
const tmpDir = server.tmpDir("uploads", "images");

// Embed data for later use
server.embed("config", { apiKey: "secret" });

// Get embedded data
const config = server.embed("config");

// Exit the server
server.exit(0);

Event Handling​

// Handle graceful shutdown
server.on("SIGTERM", () => {
console.log("Received SIGTERM, shutting down gracefully");
server.close();
});

server.on("SIGINT", () => {
console.log("Received SIGINT, shutting down gracefully");
server.close();
});

Server Shutdown​

// Graceful shutdown
async function shutdown() {
console.log("Shutting down server...");
await server.close();
console.log("Server shut down successfully");
process.exit(0);
}

// Handle shutdown signals
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);

close() (alias disconnect()) always settles within roughly timeoutMs (10 seconds by default) — any open connections are given that long to drain before being forced closed, so shutdown is never left hanging indefinitely:

// Wait up to 30s for connections to drain before forcing them closed
await server.close({ timeoutMs: 30_000 });

// Force-close immediately, don't wait at all
await server.close({ timeoutMs: 0 });

A second close() call while one is already in flight just awaits the same shutdown instead of starting a new one. See WebSockets: Shutting down for how this interacts with open WebSocket connections on each runtime.

Runtime Support​

Balda automatically detects and adapts to your runtime:

Node.js​

// Uses Node.js http module
const server = new Server({
port: 3000,
});

Bun​

// Uses Bun's HTTP server
const server = new Server({
port: 3000,
});

Deno​

// Uses Deno's HTTP server
const server = new Server({
port: 3000,
});

Beyond Node, Bun, and Deno​

server.listen() covers any host that runs one of those three runtimes - which is most container platforms (Cloud Run, Fly, Railway, Render, App Runner, a plain VM). For platforms that hand you a Request instead of letting you bind a port, use server.fetch():

// Deno Deploy / Supabase Edge Functions
Deno.serve(server.fetch);

// Vercel / Netlify functions
export default server.fetch;

AWS Lambda has its own native handle() adapter. See Deployment for the full support table and details.

Node.js HTTP Client Options​

When running on Node.js, you can specify which HTTP module to use via the nodeHttpClient option:

const server = new Server({
port: 3000,
nodeHttpClient: "http", // 'http' | 'http2' | 'https' | 'http2-secure'
});

Available Options​

ValueDescription
'http'Standard HTTP/1.1 server (default)
'http2'HTTP/2 server without TLS (cleartext, h2c)
'https'HTTPS server with TLS encryption (HTTP/1.1 over TLS)
'http2-secure'HTTP/2 server with TLS encryption (recommended for production HTTP/2)

HTTPS Configuration​

When using nodeHttpClient: 'https', you must provide the httpsOptions with your TLS certificate and key:

import fs from "fs";

const server = new Server({
port: 443,
nodeHttpClient: "https",
httpsOptions: {
key: fs.readFileSync("./certs/private-key.pem"),
cert: fs.readFileSync("./certs/certificate.pem"),
},
});

server.listen(({ url }) => {
console.log(`🔒 Secure server running at ${url}`);
});

HTTP/2 Secure Configuration​

For HTTP/2 with TLS (the standard way browsers connect via HTTP/2), use nodeHttpClient: 'http2-secure':

import fs from "fs";

const server = new Server({
port: 443,
nodeHttpClient: "http2-secure",
httpsOptions: {
key: fs.readFileSync("./certs/private-key.pem"),
cert: fs.readFileSync("./certs/certificate.pem"),
},
});

server.listen(({ url }) => {
console.log(`🚀 HTTP/2 secure server running at ${url}`);
});
info

Most browsers only support HTTP/2 over TLS, so 'http2-secure' is the recommended option for production HTTP/2 servers. The 'http2' option (cleartext h2c) is primarily useful for internal services or testing.

TLS Options​

Both 'https' and 'http2-secure' require httpsOptions. You can pass any valid Node.js TLS options:

const server = new Server({
port: 443,
nodeHttpClient: "http2-secure",
httpsOptions: {
key: fs.readFileSync("./certs/private-key.pem"),
cert: fs.readFileSync("./certs/certificate.pem"),
ca: fs.readFileSync("./certs/ca-certificate.pem"), // Certificate Authority
passphrase: "your-passphrase", // If key is encrypted
minVersion: "TLSv1.2", // Minimum TLS version
},
});
tip

For local development, you can generate self-signed certificates using tools like mkcert.

note

The httpsOptions property is only available when nodeHttpClient is set to 'https' or 'http2-secure'. TypeScript will enforce this at compile time.

Complete Example​

import { Server } from "balda";

const server = new Server({
port: 3000,
controllerPatterns: ["./src/controllers/**/*.ts"],
plugins: {
cors: { origin: "*" },
bodyParser: { json: { sizeLimit: "10mb" } },
},
});

// Global middleware
server.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});

// Error handling
server.setErrorHandler((req, res, next, error) => {
res.internalServerError({ error: error.message });
});

// Health check
server.router.get("/health", (req, res) => {
res.json({ status: "ok" });
});

// Start server
server.listen(({ port }) => {
console.log(`Server running on http://localhost:${port}`);
});