Get a server running in 30 seconds:
import {createServer} from "node:http";
import {woodland} from "woodland";
const app = woodland();
app.get("/", (req, res) => res.send("Hello World!"));
app.get("/users/:id", (req, res) => res.json({id: req.params.id}));
createServer(app.route).listen(3000, () => {
console.log("Server running at http://localhost:3000");
});That's it! You get routing, JSON responses, parameters, and more - out of the box.
Most HTTP frameworks slow you down. Woodland speeds you up.
| Feature | Woodland | Express.js | Raw Node.js |
|---|---|---|---|
| Performance | 12,478 ops/sec | 12,112 ops/sec | 10,888 ops/sec |
| Learning Curve | Express-compatible | Gentle | Steep |
| Built-in Features | CORS, ETags, Logging | Limited | None |
| TypeScript | β First-class | β | β |
β
15% faster than raw Node.js - Optimized pipeline, not overhead
β
Express-compatible - Zero learning curve, drop-in middleware
β
Zero config - Works out of the box, tune when you need to
β
Production-ready - 100% test coverage, battle-tested security
β
TypeScript first - Full type definitions included
π₯ Smart Routing: Parameter routes (:id), RegExp patterns, wildcards
π‘οΈ Security Built-in: CORS, ETags, secure defaults, injection protection
π¦ Static Files: High-performance serving with streaming
π§ Middleware: Express-compatible req, res, next pattern
π Production Logging: Common Log Format, customizable levels
π Modern JS: ES6+ modules for Node.js 17+
Security isn't optional. Woodland provides it out of the box.
Automatic Protection:
- β Injection Prevention: Input validation, HTML escaping, path traversal protection
- β Secure Defaults: CORS disabled by default, safe error handling
- β XSS Protection: All user input escaped, security headers included
- β Access Control: Strict file access, allowlist-based CORS validation
Production Setup (add these once):
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
app.always(helmet()); // Security headers
app.always(rateLimit({windowMs: 15 * 60 * 1000, max: 100})); // Rate limitingFor complete OWASP Top 10 coverage and security architecture, see the Technical Documentation.
const app = woodland({defaultHeaders: {"content-type": "application/json"}});
// Body parser middleware
app.always(async (req, res, next) => {
if (req.method === "POST" || req.method === "PUT") {
let body = "";
req.on("data", chunk => body += chunk);
req.on("end", () => {
req.body = JSON.parse(body || "{}");
next();
});
} else {
next();
}
});
// CRUD routes
const users = new Map();
app.get("/users", (req, res) => res.json(Array.from(users.values())));
app.get("/users/:id", (req, res) => {
const user = users.get(req.params.id);
user ? res.json(user) : res.error(404);
});
app.post("/users", (req, res) => {
const id = Date.now().toString();
const user = {...req.body, id};
users.set(id, user);
res.json(user, 201);
});// Allow specific origins
const app = woodland({
origins: ["https://myapp.com", "http://localhost:3000"],
corsExpose: "x-total-count,x-page-count" // Expose custom headers
});
// Woodland automatically handles:
// - Preflight OPTIONS requests
// - Access-Control-Allow-Origin headers
// - Access-Control-Allow-Methods based on your routes
// - Origin validation and security// Directory listing + file serving
const app = woodland({autoindex: true});
app.files("/", "./public"); // Serve /public folder at /const app = woodland();
// Global error handler (register last)
app.use("/(.*)", (error, req, res, next) => {
console.error(error);
res.error(500, "Internal Server Error");
});
app.get("/users/:id", (req, res, next) => {
const user = findUser(req.params.id);
if (!user) {
return res.error(404, "User not found");
}
res.json(user);
});import {Woodland} from "woodland";
class API extends Woodland {
constructor() {
super({
defaultHeaders: {"x-api-version": "1.0.0"},
origins: ["https://myapp.com"]
});
this.setupRoutes();
}
setupRoutes() {
this.get("/health", this.healthCheck);
this.get("/users", this.getUsers);
this.post("/users", this.createUser);
}
healthCheck(req, res) {
res.json({status: "ok", timestamp: new Date().toISOString()});
}
getUsers(req, res) { /* ... */ }
createUser(req, res) { /* ... */ }
}
const api = new API();# npm
npm install woodland
# yarn
yarn add woodland
# pnpm
pnpm add woodland
# Global installation for CLI
npm install -g woodland- Quick Start - Get running in 30 seconds
- Common Patterns - Real-world examples
- Configuration - Options and customization
- Routing - Routes, parameters, and patterns
- Middleware - Global and route-specific middleware
- CORS - Cross-origin resource sharing
- Static Files - File serving and directory listing
- Error Handling - Error responses and global handlers
- CLI - Command-line server
- API Reference - Complete method documentation
- Performance - Benchmarks and optimization
- TypeScript - Type definitions and usage
- Examples - Complete working examples
- Troubleshooting - Common issues and solutions
- Technical Documentation - Deep dive into architecture
Most apps need zero config, but you can customize everything:
const app = woodland(); // Defaults that workconst app = woodland({
origins: ["https://myapp.com"], // CORS allowlist
defaultHeaders: { // Security headers
"x-content-type-options": "nosniff",
"x-frame-options": "DENY"
},
logging: { // Production logging
enabled: true,
level: "info",
format: "%h %t \"%r\" %>s %b"
},
cacheSize: 5000, // Performance tuning
cacheTTL: 600000,
time: true // Response timing
});const app = woodland({
autoindex: false, // Directory browsing (false = safe)
cacheSize: 1000, // Route cache size
cacheTTL: 10000, // Cache TTL in ms
charset: "utf-8", // Default charset
corsExpose: "", // Exposed CORS headers
defaultHeaders: {}, // Default response headers
digit: 3, // Timing precision
etags: true, // Enable ETags
indexes: ["index.html"], // Index files
logging: {
enabled: true,
format: "%h %l %u %t \"%r\" %>s %b",
level: "info"
},
origins: [], // CORS origins (empty = deny all)
silent: false, // Disable default headers
time: false // X-Response-Time header
});app.get("/users", getAllUsers);
app.post("/users", createUser);
app.put("/users/:id", updateUser); // Parameter routes
app.delete("/users/:id", deleteUser);
app.get("/files/:path(.*)", serveFile); // RegExp patterns// Single parameter
app.get("/users/:id", (req, res) => {
res.json({id: req.params.id});
});
// Multiple parameters
app.get("/users/:userId/posts/:postId", (req, res) => {
res.json({userId: req.params.userId, postId: req.params.postId});
});
// Typed parameters (numeric only)
app.get("/users/:id(\\d+)", (req, res) => {
// Only matches /users/123, not /users/abc
res.json({id: parseInt(req.params.id)});
});// Authentication middleware
const authenticate = (req, res, next) => {
const token = req.headers.authorization;
if (!token) return res.error(401);
req.user = verifyToken(token);
next();
};
// Protect routes
app.get("/admin/*", authenticate, adminHandler);
app.post("/api/users", authenticate, createUser);Woodland uses the familiar req, res, next pattern. Register global middleware with always(), route-specific middleware by adding handlers to routes.
// Runs on every request
app.always((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// Multiple middleware execute in registration order
app.always(loggingMiddleware);
app.always(authMiddleware);
app.always(bodyParser);// Middleware only runs for specific routes
app.get("/protected", authenticate, authorize, handler);
app.post("/api/*", validateUser, createResource);Error middleware (4 parameters: error, req, res, next) must be registered last for each route:
// β
Correct: Error handler registered last
app.get("/users",
authenticate, // Normal middleware
getUsers, // Route handler
(error, req, res, next) => { // Error middleware - LAST
console.error(error);
res.error(500);
}
);
// Global error handler
app.use("/(.*)", (error, req, res, next) => {
if (error) {
console.error(`Error for ${req.url}:`, error);
res.error(500, "Internal Server Error");
} else {
next();
}
});
// β Don't use app.always() for error middleware
// app.always((error, req, res, next) => { ... }) // Wrong!Body Parser:
app.always(async (req, res, next) => {
if (!["POST", "PUT", "PATCH"].includes(req.method)) {
return next();
}
let body = "";
req.on("data", chunk => body += chunk);
req.on("end", () => {
try {
req.body = JSON.parse(body);
} catch (e) {
req.body = body;
}
next();
});
});Rate Limiter:
const rateLimit = (() => {
const requests = new Map();
return (req, res, next) => {
const ip = req.ip;
const now = Date.now();
const windowMs = 60000; // 1 minute
const maxRequests = 100;
if (!requests.has(ip)) {
requests.set(ip, []);
}
const recent = requests.get(ip).filter(t => now - t < windowMs);
if (recent.length >= maxRequests) {
return res.error(429);
}
recent.push(now);
requests.set(ip, recent);
next();
};
})();
app.always(rateLimit);Request Logging:
app.always((req, res, next) => {
const start = Date.now();
res.on("finish", () => {
const duration = Date.now() - start;
console.log(`${req.method} ${req.url} - ${res.statusCode} (${duration}ms)`);
});
next();
});// Serve public folder at /static
app.files("/static", "./public");
// Directory listing enabled
const app = woodland({autoindex: true});
app.files("/", "./public");
// Custom index files
const app = woodland({
autoindex: true,
indexes: ["index.html", "index.htm", "default.html"]
});// Download endpoint
app.get("/downloads/(.*)", (req, res) => {
const filename = req.params[0];
const filepath = path.join("./downloads", filename);
app.serve(req, res, filename, "./downloads");
});Most apps only need to configure origins - Woodland handles the rest.
const app = woodland({
origins: ["https://myapp.com", "http://localhost:3000"],
corsExpose: "x-total-count" // Expose custom headers to client
});
// Woodland automatically provides:
// β
Preflight OPTIONS requests
// β
Access-Control-Allow-Origin headers
// β
Access-Control-Allow-Methods based on your routes
// β
Access-Control-Allow-Credentials: true
// β
Origin validation (denies unknown origins)// Conditional CORS (manual control)
const app = woodland({origins: []}); // Disable automatic CORS
// Dynamic origin validation
app.always((req, res, next) => {
const origin = req.headers.origin;
if (isValidOrigin(origin, req.user)) {
res.header("access-control-allow-origin", origin);
res.header("access-control-allow-credentials", "true");
}
next();
});app.get("/error", (req, res) => {
res.error(500, "Server Error");
});
app.get("/not-found", (req, res) => {
res.error(404); // 404 Not Found
});
app.get("/bad-request", (req, res) => {
res.error(400, "Invalid input", {
"content-type": "application/json"
});
});// Register last, catches all errors
app.use("/(.*)", (error, req, res, next) => {
console.error(`[${res.statusCode}] ${req.url}:`, error);
if (res.statusCode >= 500) {
logError(error, req); // External logging
}
res.error(res.statusCode, "Internal Server Error");
});app.on("error", (req, res, err) => {
console.error(`Error ${res.statusCode} on ${req.url}:`, err);
});app.get("/users/:id", (req, res) => {
res.json({id: req.params.id, name: "John"});
});
app.post("/users", (req, res) => {
const user = createUser(req.body);
res.json(user, 201); // Custom status
});// Permanent redirect
app.get("/old", (req, res) => {
res.redirect("/new");
});
// Temporary redirect
app.get("/temp", (req, res) => {
res.redirect("/target", false); // false = temporary
});// Single header
app.get("/api", (req, res) => {
res.header("x-total-count", "100");
res.json({data: []});
});
// Multiple headers
app.get("/download", (req, res) => {
res.set({
"content-disposition": "attachment; filename=data.json",
"content-type": "application/json"
});
res.send(JSON.stringify({data: "example"}));
});// Log all connections
app.on("connect", (req, res) => {
console.log(`Connection from ${req.ip}`);
});
// Analytics after each request
app.on("finish", (req, res) => {
analytics.track({
method: req.method,
url: req.url,
status: res.statusCode,
ip: req.ip
});
});
// Centralized error logging
app.on("error", (req, res, err) => {
console.error(`[${res.statusCode}] ${req.url}:`, err);
if (res.statusCode >= 500) {
logErrorToService(err, req);
}
});const app = woodland({
logging: {
enabled: true,
level: "debug", // error, warn, info, debug
format: "%h %t \"%r\" %>s %b"
}
});| Placeholder | Description |
|---|---|
%h |
Remote IP |
%t |
Timestamp |
%r |
Request line |
%s |
Status code |
%b |
Response size |
%{Header}i |
Request header |
%{Header}o |
Response header |
app.log("Custom message", "info");
app.log("Debug info", "debug");Serve files quickly without writing code:
# Install globally
npm install -g woodland
# Serve current directory (default: http://127.0.0.1:8000)
woodland
# Custom port and IP
woodland --ip=0.0.0.0 --port=3000
# Disable logging
woodland --logging=false| Option | Default | Description |
|---|---|---|
--ip |
127.0.0.1 |
Server IP address |
--port |
8000 |
Server port |
--logging |
true |
Enable/disable logging |
The CLI achieves 100% test coverage with comprehensive unit tests covering argument parsing, validation, server configuration, error handling, and actual HTTP request serving.
$ woodland --port=3000
id=woodland, hostname=localhost, ip=127.0.0.1, port=3000
127.0.0.1 - [18/Dec/2024:10:30:00 -0500] "GET / HTTP/1.1" 200 1327
127.0.0.1 - [18/Dec/2024:10:30:05 -0500] "GET /favicon.ico HTTP/1.1" 404 9import {woodland} from "woodland";
const app = woodland(options);import {Woodland} from "woodland";
class MyAPI extends Woodland {
constructor() {
super(options);
}
}| Method | Description |
|---|---|
app.get(path, ...handlers) |
GET route |
app.post(path, ...handlers) |
POST route |
app.put(path, ...handlers) |
PUT route |
app.delete(path, ...handlers) |
DELETE route |
app.patch(path, ...handlers) |
PATCH route |
app.options(path, ...handlers) |
OPTIONS route |
app.trace(path, ...handlers) |
TRACE route |
app.connect(path, ...handlers) |
CONNECT route |
app.use(path, ...handlers) |
Generic middleware |
| Method | Description |
|---|---|
app.always(path, ...handlers) |
Global middleware (all requests) |
app.files(path, folder) |
Static file server |
app.ignore(fn) |
Ignore route patterns |
| Event | Description |
|---|---|
app.on("connect", handler) |
New connection |
app.on("finish", handler) |
Request completed |
app.on("error", handler) |
Error occurred |
app.on("stream", handler) |
File streaming |
| Property | Description |
|---|---|
req.allow |
Allowed methods for path |
req.body |
Request body (set by middleware) |
req.cors |
Is this a CORS request? |
req.host |
Hostname from request |
req.ip |
Client IP address |
req.params |
Route parameters |
req.parsed |
Parsed URL object |
req.valid |
Request validation status |
req.exit() |
Exit middleware chain |
| Method | Description |
|---|---|
res.error(status, body, headers) |
Send error |
res.header(key, value) |
Set header |
res.json(body, status, headers) |
Send JSON |
res.redirect(url, permanent) |
Redirect |
res.send(body, status, headers) |
Send response |
res.set(headers) |
Set multiple headers |
res.status(code) |
Set status code |
| Hook | Description |
|---|---|
onReady(req, res, body) |
Before sending response |
onSend(req, res, body) |
Customize response |
onDone(req, res, body) |
Finalize response |
Platform: Apple Mac Mini M4 Pro, Node.js 24.8.0 (1000 iterations, 5-run average)
| Framework | ops/sec | avg latency | Rank |
|---|---|---|---|
| Fastify | 14,283 | 0.070ms | π₯ |
| Woodland | 12,478 | 0.080ms | π₯ |
| Express.js | 12,112 | 0.083ms | π₯ |
| Raw Node.js | 10,888 | 0.092ms |
Woodland is 15% faster than raw Node.js, 3% faster than Express.js, 87% of Fastify's performance
- Optimized request/response pipeline (vs raw Node.js)
- Lightweight middleware system (vs Express.js)
- Built-in JSON optimization and efficient header management
- Route caching with intelligent lookup (4.8M ops/sec cached)
- Use cached routes: Route caching provides 16x improvement
- Minimize middleware: Only use what you need
- Enable ETags: Reduce bandwidth for unchanged resources
- Stream large files: Built-in streaming (330K ops/sec)
- Order routes strategically: Frequently used routes first
git clone https://github.com/avoidwork/woodland.git
cd woodland
npm install
# Run all benchmarks
npm run benchmark
# Specific suites
node benchmark.js routing utility serving
node benchmark.js --iterations 2000 --warmup 200Woodland maintains 100% statement coverage with comprehensive testing across all features. The CLI module achieves 100% coverage with rigorous testing of all code paths including successful server startup, and the utility module achieves 100% line coverage with comprehensive edge case testing.
npm test386 passing (6s)
--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
cli.js | 100 | 100 | 100 | 100 |
constants.js | 100 | 100 | 100 | 100 |
utility.js | 100 | 100 | 100 | 100 |
woodland.js | 100 | 100 | 100 | 100 |
--------------|---------|----------|---------|---------|-------------------
- CLI Tests (100% coverage) - Argument parsing, port/IP validation, server startup with HTTP verification, error handling, logging configuration, edge cases
- Security Integration Tests - Path traversal protection, IP security, CORS enforcement, autoindex security, security headers
- Constants Tests - HTTP methods, status codes, headers, content types, server info, export validation
- Security Utility Functions - File path validation, sanitization, HTML escaping, IPv4/IPv6 validation
- Utility Functions - Autoindex generation, status resolution, MIME detection, parameter parsing, URL processing, timing utilities
- Woodland Core Tests - Constructor configuration, HTTP method handlers, middleware registration, routing, CORS handling
- Stream Method Tests - File headers, different file types, range requests, ETags, binary files
- Range Request Tests - String content, invalid ranges, streams, partial content delivery
- Cache Functionality - Route caching, allows caching, cache eviction, permissions caching
- Serve Method Tests - Text files, HTML files, binary files, 404 handling, directory redirection, index files, autoindex, nested paths, large files
- Middleware Tests - Execution order, error propagation, parameterized routes, exit functionality, wildcard middleware
- Response Helper Tests - JSON responses, redirects, header manipulation, status codes, error handling
import {woodland} from "woodland";
import assert from "node:assert";
describe("My API", () => {
let app;
beforeEach(() => {
app = woodland();
});
it("should respond to GET /", async () => {
app.get("/", (req, res) => res.send("Hello"));
const req = {method: "GET", url: "/", headers: {}};
const res = {
statusCode: 200,
headers: {},
setHeader: (k, v) => res.headers[k] = v,
end: (body) => res.body = body
};
app.route(req, res);
assert.equal(res.body, "Hello");
});
});Woodland includes full TypeScript definitions:
import {Woodland, woodland} from "woodland";
import {IncomingMessage, ServerResponse} from "node:http";
// Using factory function
const app = woodland({
defaultHeaders: {"content-type": "application/json"}
});
// Using class with custom types
interface UserRequest extends IncomingMessage {
user?: {id: string; name: string};
}
const authenticate = (
req: UserRequest,
res: ServerResponse,
next: () => void
): void => {
req.user = {id: "123", name: "John"};
next();
};
app.get("/protected", authenticate, (req, res) => {
const user = (req as UserRequest).user;
res.json(user);
});// Problem: CORS blocked
// Solution: Configure origins
const app = woodland({
origins: ["https://myapp.com", "http://localhost:3000"]
});// Problem: Route not matching
// Solution: Check trailing slashes
app.get("/users/:id", handler); // β
app.get("/users/:id/", handler); // β Trailing slash// Problem: High memory
// Solution: Tune cache
const app = woodland({
cacheSize: 100, // Reduce cache
cacheTTL: 60000 // Shorter TTL
});const app = woodland({
logging: {level: "debug"}
});
app.log("Debug info", "debug");Copyright (c) 2026 Jason Mulligan
Licensed under the BSD-3-Clause license.
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass (
npm test) - Submit a pull request
- Issues: GitHub Issues
- Documentation: GitHub Wiki
- Discussions: GitHub Discussions