Conventions and standards for the Woodland HTTP framework codebase.
- JavaScript Version
- Formatting
- Naming Conventions
- Code Structure
- Security Patterns
- Testing Standards
- Documentation
- Target: ES2022
- Modules: ES modules (
import/export) - Private fields: ES2022
#syntax for encapsulation - No transpilation: Code runs directly on Node.js LTS
- Tabs for indentation (not spaces)
- Consistent tab width throughout
- Required on all statement-ending lines
- No trailing semicolons after blocks
-
Double quotes (
") for imports -
Single quotes (
') for strings in code -
Template literals for string interpolation
-
Example:
import { woodland } from "woodland"; const message = "Hello World"; const greeting = `Welcome, ${name}!`;
- Forbidden:
console.log,console.error, etc. - Use
app.logger.log()for application logging - Lint rule:
no-console: error
-
Forbidden: Raw numeric literals (0, 1, -1, etc.)
-
Forbidden: Raw string literals ("function", "/", etc.)
-
Required: Use constants from
constants.js -
Example:
// Good if (count === INT_0) { return EMPTY; } for (let i = INT_0; i < length; i++) { process(items[i]); } if (typeof fn === FUNCTION) { fn(); } const first = array[INT_0]; // Bad if (count === 0) { return ""; } for (let i = 0; i < length; i++) { process(items[i]); } if (typeof fn === "function") { fn(); } const first = array[0];
- Prefix with underscore (
_) when unused - Example:
app.get("/test", (req, res, _next) => { res.json({ ok: true }); });
- Lowercase with hyphens or underscores
- Descriptive names:
woodland.js,fileserver.js,middleware.js
- PascalCase:
Woodland,FileServer
- camelCase:
createLogger,validateConfig,extractIP
- UPPER_SNAKE_CASE:
SLASH,EMPTY,INT_0,GET,STATUS_CODES - Numeric constants:
INT_0,INT_1,INT_NEG_1,INT_65535, etc. - String constants:
FUNCTION,STRING,DOUBLE_SLASH,SLASH_BACKSLASH - Array indices: Use
INT_0,INT_1, etc. instead of raw numbers - See No Magic Values for usage examples
- ES2022
#prefix:#cache,#logger,#middleware - Private methods also use
#:#decorate(),#onReady()
- camelCase:
validated,resolvedFolder,middlewareArray - Single-letter for counters:
i,j,len
All internal state uses ES2022 private fields:
class Woodland extends EventEmitter {
#cache;
#logger;
#middleware;
constructor(config) {
super();
this.#cache = lru(1000, 10000);
this.#logger = createLogger(config.logging);
}
}Use factories for object creation:
export function createLogger(config) {
return Object.freeze({
log: (msg) => console.log(msg),
});
}
export function createMiddlewareRegistry(methods, cache) {
return {
register: (path, ...fn) => {},
allowed: (method, uri) => {},
};
}- Freeze public objects:
Object.freeze() - Return copies, not references
- Example:
this.#indexes = [...indexes]; // Copy array this.#logger = Object.freeze(logger); // Freeze object
Public methods return this for chaining:
use(rpath, ...fn) {
this.#middleware.register(rpath, ...fn);
return this;
}
get(...args) {
return this.use(...args, GET);
}Prefer for loops in hot paths:
// Preferred - with constants and cached length
const itemCount = array.length;
for (let i = INT_0; i < itemCount; i++) {
const item = array[i];
}
// Avoid in hot paths
for (const item of array) {
}Cache .length lookups in loop conditions for better performance:
// Good - cached length
const entryCount = entries.length;
for (let i = INT_0; i < entryCount; i++) {
const [key, value] = entries[i];
}
// Bad - length accessed on every iteration
for (let i = INT_0; i < entries.length; i++) {
const [key, value] = entries[i];
}Why: Accessing .length on each iteration adds unnecessary property lookups. Cache it once before the loop.
Use destructuring for cleaner code:
// Good
const [key, value] = entries[i];
const { name, size } = file;
// Bad
const key = entry[0];
const value = entry[1];
const name = file.name;
const size = file.size;Use modern JavaScript features for safer access:
// Good
const port = config?.port ?? INT_8000;
const host = options?.host ?? LOCALHOST;
// Bad
const port = options && options.port ? options.port : INT_8000;
const host = options && options.host ? options.host : LOCALHOST;Always validate file paths with boundary checks:
const resolvedFolder = resolve(folder);
const isWithin =
fp === resolvedFolder || (fp.startsWith(resolvedFolder) && fp[resolvedFolder.length] === sep);
if (!isWithin) {
res.error(INT_403);
return;
}Key points:
- Use
path.sepfor cross-platform compatibility - Check boundary character, not just
startsWith - Handle exact matches (
fp === resolvedFolder) - Use constants for status codes (
INT_403not403)
Escape all user output:
import { escapeHtml } from "./response.js";
const safeName = escapeHtml(fileName);
params[key] = coerce(escapeHtml(decoded));Empty origins array = deny all:
if (origins.size === INT_0) {
return false; // Deny CORS
}Validate IPs before use:
if (!isValidIP(ip)) {
return fallbackIP;
}No sensitive data in error responses:
res.error(status, new Error(STATUS_CODES[status]));
// Never: res.error(500, err.stack)import { describe, it } from "node:test";
import assert from "node:assert";
describe("module", () => {
it("should do something", async () => {
const result = await someFunction();
assert.strictEqual(result, expected);
});
});For HTTP tests, mock responses must include:
send(),json(),end(),pipe(),on(),emit()methodssocket.server._connectionKeyfor CORS/IP extraction- Destroy file streams to prevent EMFILE errors
- 100% line coverage (required)
- 99%+ function coverage (current: 99.37%)
- 95%+ branch coverage (current: 95.90%)
Always test:
- Path traversal:
../../../etc/passwd - Sibling bypass:
../public2/file.txt - Boundary conditions: exact matches vs. prefix matches
All public functions and classes:
/**
* Creates a new Woodland instance
* @param {Object} [config={}] - Configuration object
* @param {boolean} [config.autoIndex=false] - Enable directory indexing
* @returns {Woodland} New Woodland instance
*/
export function woodland(config = {}) {
return new Woodland(config);
}Use proper type annotations for complex types:
/**
* @param {Object} req - Request object
* @param {Object} res - Response object
* @param {Object} [headers={}] - Response headers
* @returns {Object} Response object
*/When adding new constants, document their purpose:
// Numeric constants
export const INT_0 = 0;
export const INT_1 = 1;
export const INT_NEG_1 = -1;
export const INT_65535 = 65535;
// String constants
export const FUNCTION = "function";
export const DOUBLE_SLASH = "//";
export const SLASH_BACKSLASH = "/\\";See constants.js for the complete list of available constants.
Use sparingly, only for complex logic:
// Path traversal protection: ensure fp is within resolvedFolder
// Must match exactly or be a subdirectory (not a sibling)
const isWithin =
fp === resolvedFolder || (fp.startsWith(resolvedFolder) && fp[resolvedFolder.length] === sep);Note: Don't duplicate code in comments - let the code speak for itself when possible.
no-console: error- No console statementsno-unused-vars: error- No unused variables- Prefix unused params with
_
npm run lint # Check linting
npm run fix # Auto-fix issues (lint + format)
npm run coverage # Verify 100% line coverage- Make changes
- Run
npm run fix(fixes lint + formatting) - Run
npm run coverage(verifies 100% line coverage) - Run
npm run build(generates dist files) - Commit only when explicitly requested
Pre-commit check: npm run fix && npm run coverage && npm run build
Last updated: April 2026