Woodland
High-performance HTTP frameworkπ Features
- π Performance Leader: 29% faster than raw Node.js, 56% faster than Express.js, competitive with Fastify - proven by benchmarks
- β‘ Zero Overhead: Framework features with performance gains, not costs
- π Security First: Built-in CORS, ETags, and comprehensive security headers
- π€οΈ Smart Routing: Parameter syntax (
/users/:id
) and RegExp support with caching - π§ Express Compatible: Familiar middleware with
req, res, next
pattern - π File Serving: High-performance static file server with streaming
- π TypeScript Ready: Full TypeScript definitions included
- π Production Logging: Common Log Format with customizable levels
- π Modern Architecture: ES6+ modules optimized for Node.js 17+
π Security & OWASP Compliance
Woodland follows a security-first design philosophy with strong adherence to OWASP guidelines:
- β Injection Prevention: Comprehensive input validation, HTML escaping, and path traversal protection
- β Secure Defaults: CORS disabled by default, autoindex disabled, secure error handling
- β Access Control: Strict file access controls and allowlist-based CORS validation
- β XSS Protection: All user input properly escaped, security headers included
- π‘οΈ Security Headers:
X-Content-Type-Options: nosniff
set automatically,helmet
recommended for comprehensive headers - β‘ Rate Limiting: Built for middleware compatibility - use
express-rate-limit
or similar - π Comprehensive Testing: 100+ dedicated security tests covering attack vectors and edge cases
OWASP Top 10 Coverage: Excellent protection against injection attacks, broken access control, security misconfigurations, and cross-site scripting. See Technical Documentation for complete assessment.
π‘ Quick Security Setup: Add essential security middleware for production deployment:
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
// Security headers
app.use(helmet());
// Rate limiting
app.use(rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
}));
π‘ Why Choose Woodland?
Stop accepting framework overhead. Most HTTP frameworks slow you down in exchange for convenience. Woodland breaks that trade-off.
π Proven Performance: Comprehensive benchmarks show Woodland outperforms raw Node.js by 29%, Express.js by 56%, and is competitive with Fastify
β‘ Zero Compromise: Get all the framework features you need with better performance than hand-coding
π Battle-Tested: 100% statement coverage with 416 comprehensive tests, production-ready security, and enterprise-grade reliability
π§ Developer Experience: Express-compatible API means zero learning curve for your team
The Result? Your applications run faster, your servers handle more traffic, and your infrastructure costs less.
π¦ Installation
# npm
npm install woodland
# yarn
yarn add woodland
# pnpm
pnpm add woodland
# Global installation for CLI
npm install -g woodland
π Quick Start
Basic Server
import {createServer} from "node:http";
import {woodland} from "woodland";
const app = woodland({
defaultHeaders: {
"cache-control": "public, max-age=3600",
"content-type": "text/plain"
},
time: true
});
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.get("/users/:id", (req, res) => {
res.json({
id: req.params.id,
name: `User ${req.params.id}`
});
});
createServer(app.route).listen(3000, () => {
console.log("Server running on http://localhost:3000");
});
Using the Class
import {Woodland} from "woodland";
class MyAPI extends Woodland {
constructor() {
super({
defaultHeaders: {
"x-api-version": "1.0.0"
},
origins: ["https://myapp.com"]
});
this.setupRoutes();
}
setupRoutes() {
this.get("/api/health", this.healthCheck);
this.post("/api/users", this.createUser);
}
healthCheck(req, res) {
res.json({status: "healthy", timestamp: new Date().toISOString()});
}
createUser(req, res) {
// Handle user creation
res.status(201).json({message: "User created"});
}
}
const api = new MyAPI();
π Table of Contents
- Configuration
- Routing
- Middleware
- Static Files
- CORS
- Error Handling
- Response Helpers
- Event Handlers
- Logging
- CLI Usage
- API Reference
- Performance
- Testing
- TypeScript
- Examples
- Troubleshooting
βοΈ Configuration
Default Configuration
const app = woodland({
autoindex: false, // Enable directory browsing
cacheSize: 1000, // Internal cache size
cacheTTL: 10000, // Cache TTL (10 seconds)
charset: "utf-8", // Default charset
corsExpose: "", // CORS exposed headers
defaultHeaders: {}, // Default response headers
digit: 3, // Timing precision digits
etags: true, // Enable ETag generation
indexes: [ // Index file names
"index.htm",
"index.html"
],
logging: {
enabled: true, // Enable logging
format: "%h %l %u %t \"%r\" %>s %b", // Log format
level: "info" // Log level
},
origins: [], // CORS origins (empty array denies all cross-origin requests)
silent: false, // Disable default headers
time: false // Enable response timing
});
Advanced Configuration
const app = woodland({
// Security headers
defaultHeaders: {
"x-content-type-options": "nosniff",
"x-frame-options": "DENY",
"x-xss-protection": "1; mode=block",
"strict-transport-security": "max-age=31536000; includeSubDomains"
},
// CORS configuration
origins: [
"https://myapp.com",
"https://api.myapp.com"
],
corsExpose: "x-custom-header,x-request-id",
// Performance tuning
cacheSize: 5000,
cacheTTL: 600000, // 10 minutes
// Detailed logging
logging: {
enabled: true,
level: "debug",
format: "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\""
},
// Enable features
time: true,
etags: true,
autoindex: true
});
π€οΈ Routing
Basic Routes
// HTTP methods
app.get("/users", getAllUsers);
app.post("/users", createUser);
app.put("/users/:id", updateUser);
app.delete("/users/:id", deleteUser);
app.patch("/users/:id", patchUser);
app.options("/users", optionsHandler);
// Route parameters
app.get("/users/:id", (req, res) => {
const userId = req.params.id;
res.json({id: userId});
});
// Multiple parameters
app.get("/users/:userId/posts/:postId", (req, res) => {
const {userId, postId} = req.params;
res.json({userId, postId});
});
Advanced Routing
// RegExp patterns
app.get("/api/v[1-3]/users", (req, res) => {
res.json({version: req.url.match(/v(\d)/)[1]});
});
// Wildcard routes
app.get("/files/(.*)", (req, res) => {
// Serve any file under /files/
});
// Route with validation
app.get("/users/:id(\\d+)", (req, res) => {
// Only matches numeric IDs
res.json({id: parseInt(req.params.id)});
});
Route Groups
// API v1 routes
const apiV1 = (req, res, next) => {
req.version = "v1";
next();
};
app.get("/api/v1/users", apiV1, getAllUsers);
app.post("/api/v1/users", apiV1, createUser);
// Protected routes
const authenticate = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.error(401);
}
// Verify token...
next();
};
app.get("/admin/*", authenticate, adminHandler);
π§ Middleware
Basic Middleware
// Global middleware
app.always((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// Route-specific middleware
app.get("/protected", authenticate, authorize, handler);
// β WRONG: Do NOT register error middleware with 'always'
// This will execute BEFORE route handlers, not after errors occur
app.always((error, req, res, next) => {
if (error) {
console.error(error);
res.error(500);
} else {
next();
}
});
// β
CORRECT: Register error middleware with specific routes LAST
app.get("/api/users",
authenticate, // Normal middleware
authorize, // Normal middleware
getUserHandler, // Route handler
(error, req, res, next) => { // Error middleware - LAST
if (error) {
console.error(error);
res.error(500);
} else {
next();
}
}
);
// β
CORRECT: Global error handling should be done with route patterns
app.use("/(.*)", (error, req, res, next) => {
if (error) {
console.error(`Global error for ${req.url}:`, error);
res.error(500, "Internal Server Error");
} else {
next();
}
});
Important Notes:
- Error middleware (functions with 4 parameters:
error, req, res, next
) should never be registered withapp.always()
- Error middleware registered with
always
will execute before route handlers, making them ineffective for catching route errors .use()
without a method defaults to GET - it behaves like.get()
, not like.always()
- Always register error middleware last in the middleware chain for each route
- For global error handling, use
app.use("/(.*)", errorHandler)
as the last route registration
Middleware Examples
// Request logging
const requestLogger = (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();
};
// Body parser
const bodyParser = async (req, res, next) => {
if (req.method === "POST" || req.method === "PUT") {
let body = "";
req.on("data", chunk => body += chunk);
req.on("end", () => {
try {
req.body = JSON.parse(body);
} catch (e) {
req.body = body;
}
next();
});
} else {
next();
}
};
// Rate limiting
const rateLimit = (() => {
const requests = new Map();
return (req, res, next) => {
const ip = req.ip;
const now = Date.now();
const window = 60000; // 1 minute
const limit = 100;
if (!requests.has(ip)) {
requests.set(ip, []);
}
const ipRequests = requests.get(ip);
const recentRequests = ipRequests.filter(time => now - time < window);
if (recentRequests.length >= limit) {
return res.error(429);
}
recentRequests.push(now);
requests.set(ip, recentRequests);
next();
};
})();
π Static Files
Basic File Serving
// Serve files from public directory
app.files("/static", "./public");
// Serve with custom options
app.get("/downloads/(.*)", (req, res) => {
const filename = req.params[0];
const filepath = path.join("./downloads", filename);
// Custom file serving logic
app.serve(req, res, filename, "./downloads");
});
Directory Browsing
const app = woodland({
autoindex: true, // Enable directory browsing
indexes: ["index.html", "index.htm", "default.html"]
});
app.files("/", "./public");
π CORS
Woodland handles CORS automatically when you configure origins. Hereβs what you get for free:
const app = woodland({
origins: ["https://myapp.com", "https://api.myapp.com"],
corsExpose: "x-total-count,x-page-count"
});
// Woodland automatically provides:
// β
Preflight OPTIONS route for all paths
// β
Access-Control-Allow-Origin header (set to request origin if allowed)
// β
Access-Control-Allow-Credentials: true
// β
Access-Control-Allow-Methods (based on registered routes)
// β
Access-Control-Allow-Headers (for OPTIONS requests)
// β
Access-Control-Expose-Headers (for non-OPTIONS requests)
// β
Timing-Allow-Origin header
// β
Origin validation and security
What Woodland Does Automatically
- Preflight Route Registration: When origins are configured, Woodland automatically registers an OPTIONS handler that responds with 204 No Content
- CORS Headers: For valid cross-origin requests, automatically sets all required CORS headers
- Origin Validation: Checks request origin against configured allowed origins
- Method Detection: Access-Control-Allow-Methods reflects actual registered routes
- Security: Empty origins array denies all CORS requests by default
Manual CORS Control (When Needed)
// Override automatic behavior for specific routes
app.options("/api/special", (req, res) => {
res.header("access-control-allow-methods", "GET,POST"); // Restrict methods
res.header("access-control-allow-headers", "content-type"); // Restrict headers
res.header("access-control-max-age", "86400"); // Set cache duration
res.send("");
});
// Dynamic origin validation (replaces automatic validation)
app.always((req, res, next) => {
const origin = req.headers.origin;
// Custom logic for origin validation
if (isValidOriginForUser(origin, req.user)) {
res.header("access-control-allow-origin", origin);
}
next();
});
// Conditional CORS (disable automatic CORS, use manual)
const app = woodland({
origins: [] // Empty = no automatic CORS
});
app.always((req, res, next) => {
if (shouldAllowCORS(req)) {
res.header("access-control-allow-origin", req.headers.origin);
res.header("access-control-allow-credentials", "true");
}
next();
});
Most applications only need to configure origins
and corsExpose
- manual CORS handling is rarely necessary.
β Error Handling
Built-in Error Handling
app.get("/error", (req, res) => {
res.error(500, "Internal Server Error");
});
app.get("/not-found", (req, res) => {
res.error(404);
});
app.get("/custom-error", (req, res) => {
res.error(400, "Bad Request", {
"content-type": "application/json"
});
});
Custom Error Handler
app.on("error", (req, res, err) => {
console.error(`Error ${res.statusCode}: ${err}`);
// Log to external service
if (res.statusCode >= 500) {
logError(err, req);
}
});
// Global error catching
app.always((req, res, next) => {
try {
next();
} catch (error) {
res.error(500, error.message);
}
});
π€ Response Helpers
JSON Responses
app.get("/users/:id", (req, res) => {
const user = {id: req.params.id, name: "John Doe"};
res.json(user);
});
app.post("/users", (req, res) => {
const user = createUser(req.body);
res.json(user, 201);
});
Redirects
app.get("/old-path", (req, res) => {
res.redirect("/new-path");
});
app.get("/temporary", (req, res) => {
res.redirect("/permanent", false); // Temporary redirect
});
Custom Headers
app.get("/api/data", (req, res) => {
res.header("x-total-count", "100");
res.header("x-page", "1");
res.json({data: []});
});
app.get("/download", (req, res) => {
res.set({
"content-disposition": "attachment; filename=data.json",
"content-type": "application/json"
});
res.send(JSON.stringify({data: "example"}));
});
π― Event Handlers
Available Events
// Connection established
app.on("connect", (req, res) => {
console.log(`Connection from ${req.ip}`);
});
// Request finished
app.on("finish", (req, res) => {
console.log(`Request completed: ${req.method} ${req.url}`);
});
// Error occurred
app.on("error", (req, res, err) => {
console.error(`Error: ${err}`);
});
// File streaming
app.on("stream", (req, res) => {
console.log(`Streaming file to ${req.ip}`);
});
Event-Driven Analytics
app.on("finish", (req, res) => {
const metrics = {
method: req.method,
url: req.url,
status: res.statusCode,
ip: req.ip,
userAgent: req.headers["user-agent"],
timestamp: new Date().toISOString()
};
// Send to analytics service
analytics.track(metrics);
});
π Logging
Log Levels
error
: Error messageswarn
: Warning messagesinfo
: Informational messagesdebug
: Debug messages
Custom Logging
const app = woodland({
logging: {
enabled: true,
level: "debug",
format: "%h %l %u %t \"%r\" %>s %b %D"
}
});
// Manual logging
app.log("Custom message", "info");
app.log("Debug information", "debug");
Log Format Placeholders
Placeholder | Description |
---|---|
%h |
Remote IP address |
%l |
Remote logname (always - ) |
%u |
Remote user (always - ) |
%t |
Timestamp |
%r |
First line of request |
%s |
Status code |
%b |
Response size |
%{Header}i |
Request header |
%{Header}o |
Response header |
π» CLI Usage
Basic Usage
# Serve current directory
woodland
# Custom IP and port
woodland --ip=0.0.0.0 --port=3000
# Disable logging
woodland --logging=false
# Serve specific directory
cd /path/to/files && woodland
The CLI achieves 100% test coverage with comprehensive unit tests covering argument parsing, validation, server configuration, error handling scenarios, and actual HTTP request serving verification.
CLI Options
Option | Default | Description |
---|---|---|
--ip |
127.0.0.1 |
Server IP address |
--port |
8000 |
Server port |
--logging |
true |
Enable/disable request logging |
Example Output
$ 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 9
π API Reference
Woodland Class
Constructor
new Woodland(config)
HTTP Methods
get(path, ...middleware)
- GET requestspost(path, ...middleware)
- POST requestsput(path, ...middleware)
- PUT requestsdelete(path, ...middleware)
- DELETE requestspatch(path, ...middleware)
- PATCH requestsoptions(path, ...middleware)
- OPTIONS requeststrace(path, ...middleware)
- TRACE requestsconnect(path, ...middleware)
- CONNECT requests
Utility Methods
always(path, ...middleware)
- All HTTP methodsuse(path, ...middleware, method)
- Generic middleware registrationfiles(root, folder)
- Static file servingignore(fn)
- Ignore middleware forAllow
headerallowed(method, uri)
- Check if method is allowedallows(uri)
- Get allowed methods stringlist(method, type)
- List registered routeslog(message, level)
- Log a message
Lifecycle Hooks
onReady(req, res, body, status, headers)
- Before sending responseonSend(req, res, body, status, headers)
- Customize responseonDone(req, res, body, headers)
- Finalize response
Request Object Extensions
req.allow
- Allowed methods for current pathreq.body
- Request body (populate with middleware)req.cors
- Boolean indicating CORS requestreq.corsHost
- Boolean indicating βoriginβ and βhostβ request headers are in syncreq.host
- Request hostnamereq.ip
- Client IP addressreq.params
- Route parametersreq.parsed
- Parsed URL objectreq.valid
- Request validation statusreq.exit()
- Exit middleware chain
Response Object Extensions
res.locals
- Local variables objectres.error(status, body, headers)
- Send error responseres.header(key, value)
- Set response headerres.json(body, status, headers)
- Send JSON responseres.redirect(url, permanent)
- Send redirect responseres.send(body, status, headers)
- Send responseres.set(headers)
- Set multiple headersres.status(code)
- Set status code
β‘ Performance
π Framework Performance Showdown
Proven Performance Leadership: Woodland delivers exceptional performance that outpaces raw Node.js and Express.js, while remaining competitive with Fastify.
Framework Comparison (JSON Response) - Averaged across 5 runs
Fastify framework: 14,452 ops/sec (0.069ms avg) π₯ FASTEST
Woodland framework: 14,173 ops/sec (0.071ms avg) π₯ Very close second
Raw Node.js HTTP module: 11,061 ops/sec (0.090ms avg) π₯ Third place
Express.js framework: 9,374 ops/sec (0.107ms avg)
Performance improvement: +28% faster than raw Node.js, +51% faster than Express.js, 98% of Fastify's performance
Why Woodland delivers exceptional performance:
- vs Raw Node.js: Optimized request/response pipeline that eliminates common inefficiencies
- vs Express.js: Lightweight middleware system without Expressβs overhead and legacy bloat
- vs Fastify: Performance-first architecture with competitive JSON handling and intelligent optimizations
- Built-in JSON response optimization with smart serialization
- Efficient header management and intelligent caching strategies
- Performance-first architecture designed from the ground up for speed
Benchmark Results
Node.js 23.10.0 on Apple M4 Pro Mac Mini (1000 iterations, 100 warmup, averaged across 5 runs)
Routing Operations
Allowed methods: 4,797,153 ops/sec (0.0002ms avg)
Path conversion: 2,561,369 ops/sec (0.0004ms avg)
Parameter routes: 2,416,581 ops/sec (0.0004ms avg)
Static routes: 2,467,653 ops/sec (0.0004ms avg)
Not found routes: 2,479,326 ops/sec (0.0004ms avg)
Route caching: 1,388,793 ops/sec (0.0007ms avg)
Allowed cache: 2,092,956 ops/sec (0.0005ms avg)
Route resolution: 917,799 ops/sec (0.0011ms avg)
Utility Operations
Number padding: 7,695,461 ops/sec (0.0001ms avg)
MIME detection: 4,619,160 ops/sec (0.0002ms avg)
Middleware chain: 3,759,994 ops/sec (0.0003ms avg)
Time formatting: 3,512,863 ops/sec (0.0003ms avg)
Content pipeability: 3,214,295 ops/sec (0.0003ms avg)
URL parsing: 2,838,552 ops/sec (0.0004ms avg)
Status determination: 3,095,503 ops/sec (0.0003ms avg)
Timezone offset: 4,638,018 ops/sec (0.0002ms avg)
Parameter extraction: 1,059,064 ops/sec (0.0009ms avg)
Directory listing: 470,633 ops/sec (0.0021ms avg)
File Serving Operations
Static file serving: 582,978 ops/sec (0.0017ms avg)
Stream operations: 330,495 ops/sec (0.0030ms avg)
ETag generation: 336,330 ops/sec (0.0030ms avg)
Stream with ETags: 332,530 ops/sec (0.0030ms avg)
HEAD requests: 68,935 ops/sec (0.015ms avg)
Small files: 39,396 ops/sec (0.025ms avg)
Large files: 42,231 ops/sec (0.024ms avg)
Medium files: 41,680 ops/sec (0.024ms avg)
Directory listing: 19,076 ops/sec (0.052ms avg)
Directory autoindex: 18,224 ops/sec (0.055ms avg)
HTTP Operations
Server startup: 113,810 ops/sec (0.009ms avg)
DELETE requests: 15,687 ops/sec (0.064ms avg)
Complex middleware: 14,593 ops/sec (0.069ms avg)
Nested routes: 14,368 ops/sec (0.070ms avg)
404 handling: 12,816 ops/sec (0.078ms avg)
Parameterized routes: 13,629 ops/sec (0.073ms avg)
JSON response: 12,568 ops/sec (0.080ms avg)
Error handling: 12,631 ops/sec (0.079ms avg)
PUT requests: 11,169 ops/sec (0.090ms avg)
Middleware chain: 10,993 ops/sec (0.091ms avg)
Mixed workload: 10,916 ops/sec (0.092ms avg)
POST requests: 10,595 ops/sec (0.094ms avg)
Simple GET: 12,515 ops/sec (0.080ms avg)
Large response: 922 ops/sec (1.084ms avg)
Performance Tips
- Choose Woodland over alternatives: Woodland provides 28% better performance than raw Node.js and 51% better than Express.js for JSON responses
- Enable Route Caching: Route caching provides significant performance improvement - allows() with cache: 4.8M ops/sec vs without: 300K ops/sec
- Optimize Route Order: Place frequently accessed routes first in your application
- Use Parameter Routes: Parameter routes perform competitively with static routes (~2.4M vs ~2.5M ops/sec)
- Enable ETags: Reduces bandwidth for unchanged resources (333K ops/sec with ETags)
- Stream Large Files: Use built-in streaming for files (330K ops/sec streaming performance)
- Minimize Middleware: Only use necessary middleware - complex middleware reduces performance
- Leverage Built-in Utilities: Use woodlandβs optimized utility functions (7.7M+ ops/sec for common operations)
- Configure Appropriate Caching: Set proper cache headers and TTL values
- Use Proper HTTP Methods: DELETE requests show best performance (15.7K ops/sec) for CRUD operations
Running Benchmarks
git clone https://github.com/avoidwork/woodland.git
cd woodland
npm install
# Run all benchmarks
npm run benchmark
# Run specific benchmark suites
node benchmark.js routing utility serving
node benchmark.js http middleware comparison
# Run with custom settings
node benchmark.js --iterations 2000 --warmup 200
# Run specific suite with custom settings
node benchmark.js utility -i 500 -w 50
Available benchmark suites:
comparison
- Framework vs raw Node.js HTTP module performancehttp
- End-to-end HTTP server performancemiddleware
- Middleware registration and executionrouting
- Route matching and resolutionserving
- File serving and streamingutility
- Core utility functions
π§ͺ Testing
Test Coverage
Woodland 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 test
Test Results
386 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 |
--------------|---------|----------|---------|---------|-------------------
Test Categories
- 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
Writing Tests
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");
});
});
π TypeScript
Type 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
class MyAPI extends Woodland {
constructor() {
super({time: true});
}
}
// Custom middleware with types
interface CustomRequest extends IncomingMessage {
user?: {id: string, name: string};
}
const authenticate = (
req: CustomRequest,
res: ServerResponse,
next: () => void
): void => {
req.user = {id: "123", name: "John"};
next();
};
Configuration Types
interface WoodlandConfig {
autoindex?: boolean;
cacheSize?: number;
cacheTTL?: number;
charset?: string;
corsExpose?: string;
defaultHeaders?: Record<string, string>;
digit?: number;
etags?: boolean;
indexes?: string[];
logging?: {
enabled?: boolean;
format?: string;
level?: string;
};
origins?: string[];
silent?: boolean;
time?: boolean;
}
π Examples
REST API
import {createServer} from "node:http";
import {woodland} from "woodland";
const app = woodland({
defaultHeaders: {"content-type": "application/json"},
time: true
});
const users = new Map();
// 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", () => {
try {
req.body = JSON.parse(body);
} catch (e) {
return res.error(400, "Invalid JSON");
}
next();
});
} else {
next();
}
});
// Routes
app.get("/users", (req, res) => {
res.json(Array.from(users.values()));
});
app.get("/users/:id", (req, res) => {
const user = users.get(req.params.id);
if (!user) {
return res.error(404, "User not found");
}
res.json(user);
});
app.post("/users", (req, res) => {
const {name, email} = req.body;
if (!name || !email) {
return res.error(400, "Name and email required");
}
const id = Date.now().toString();
const user = {id, name, email};
users.set(id, user);
res.json(user, 201);
});
app.put("/users/:id", (req, res) => {
const user = users.get(req.params.id);
if (!user) {
return res.error(404, "User not found");
}
Object.assign(user, req.body);
res.json(user);
});
app.delete("/users/:id", (req, res) => {
if (!users.has(req.params.id)) {
return res.error(404, "User not found");
}
users.delete(req.params.id);
res.status(204).send("");
});
createServer(app.route).listen(3000);
File Upload API
import {createServer} from "node:http";
import {woodland} from "woodland";
import {createWriteStream} from "node:fs";
import {pipeline} from "node:stream/promises";
const app = woodland();
app.post("/upload", async (req, res) => {
try {
const filename = req.headers["x-filename"] || "upload.bin";
const writeStream = createWriteStream(`./uploads/${filename}`);
await pipeline(req, writeStream);
res.json({message: "Upload successful", filename});
} catch (error) {
res.error(500, "Upload failed");
}
});
createServer(app.route).listen(3000);
WebSocket Integration
import {createServer} from "node:http";
import {WebSocketServer} from "ws";
import {woodland} from "woodland";
const app = woodland();
const server = createServer(app.route);
const wss = new WebSocketServer({server});
app.get("/", (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head><title>WebSocket Test</title></head>
<body>
<script>
const ws = new WebSocket('ws://localhost:3000');
ws.onmessage = e => console.log('Received:', e.data);
ws.onopen = () => ws.send('Hello Server!');
</script>
</body>
</html>
`);
});
wss.on("connection", (ws) => {
ws.send("Welcome to WebSocket server!");
ws.on("message", (data) => {
console.log("Received:", data.toString());
});
});
server.listen(3000);
π§ Troubleshooting
Common Issues
CORS Errors
// Problem: CORS blocked requests
// Solution: Configure origins properly
const app = woodland({
origins: ["https://myapp.com", "http://localhost:3000"]
});
Route Not Found
// Problem: Routes not matching
// Solution: Check route patterns
app.get("/users/:id", handler); // β
Correct
app.get("/users/:id/", handler); // β Trailing slash
app.get("/users/([0-9]+)", handler); // β
RegExp pattern
Middleware Order
// The 'routes' method builds middleware execution order as follows:
// 1. Always middleware (WILDCARD) - added first to the middleware array
// 2. Route-specific middleware - added after all always middleware
// β
Understanding the routes method behavior:
app.always(corsHandler); // Added to WILDCARD middleware map
app.always(requestLogger); // Added to WILDCARD middleware map
app.post("/users", authenticate, createUser); // Added to POST middleware map
// When routes("/users", "POST") is called, the middleware array becomes:
// [corsHandler, requestLogger, authenticate, createUser]
// with exit point set between requestLogger and authenticate
// β
Always middleware executes first regardless of registration order:
app.post("/api/users", validate, createUser); // Route registered first
app.always(securityHeaders); // Always middleware registered after
app.always(bodyParser); // Another always middleware
// Execution order for POST /api/users:
// 1. securityHeaders (always middleware)
// 2. bodyParser (always middleware)
// 3. validate (route middleware)
// 4. createUser (route handler)
// β Common misconception - registration order between always/route doesn't matter:
// The routes method ALWAYS puts always middleware first in the execution chain
Memory Issues
// Problem: High memory usage
// Solution: Tune cache settings
const app = woodland({
cacheSize: 100, // Reduce cache size
cacheTTL: 60000 // Shorter TTL
});
Debug Mode
const app = woodland({
logging: {
enabled: true,
level: "debug"
}
});
// Enable debug logs
app.log("Debug message", "debug");
Performance Issues
- Check middleware overhead: Profile middleware execution
- Optimize route patterns: Use specific patterns vs wildcards
- Enable caching: Use ETags and cache headers
- Monitor memory: Watch for memory leaks in long-running apps
π License
Copyright (c) 2025 Jason Mulligan
Licensed under the BSD-3-Clause license.
π€ Contributing
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
π Support
- Issues: GitHub Issues
- Documentation: GitHub Wiki
- Discussions: GitHub Discussions