https://github.com/avoidwork/woodland
Lightweight HTTP framework with automatic headers
https://github.com/avoidwork/woodland
Last synced: 10 months ago
JSON representation
Lightweight HTTP framework with automatic headers
- Host: GitHub
- URL: https://github.com/avoidwork/woodland
- Owner: avoidwork
- License: bsd-3-clause
- Created: 2016-06-29T22:38:03.000Z (over 9 years ago)
- Default Branch: master
- Last Pushed: 2024-04-11T01:30:41.000Z (almost 2 years ago)
- Last Synced: 2024-04-14T00:38:31.664Z (almost 2 years ago)
- Language: JavaScript
- Homepage:
- Size: 1.46 MB
- Stars: 2
- Watchers: 2
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README

# Woodland
Lightweight HTTP framework with automatic headers. Routes can use parameter syntax, i.e. `/users/:id`, or `RegExp` syntax. Route parameters are not sanitized. If 2+ routes with parameters match a request the first route will be used to extract parameters. All HTTP methods are supported.
`CORS` (Cross Origin Resource Sharing) is automatically handled, and indicated with `cors` Boolean on the `request` Object for middleware.
Middleware arguments can be `req, res, next` or `error, req, res, next`. If no `Error` handling middleware is registered woodland will handle it.
## Using the factory
```javascript
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("Custom greeting at '/:user', try it out!"));
app.get("/:user", (req, res) => res.send(`Hello ${req.params.user}!`));
createServer(app.route).listen(8000);
```
## Using the Class
```javascript
import {Woodland} from "woodland";
class MyFramework extends Woodland {};
```
## Testing
Woodland has >99% code coverage with its tests.
```console
--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 99.79 | 74.1 | 98.59 | 100 |
woodland.cjs | 99.79 | 74.1 | 98.59 | 100 | ...
--------------|---------|----------|---------|---------|-------------------
```
## Benchmark
Please benchmark `woodland` on your target hardware to understand the overhead which is expected to be <15% with etags disabled, or <25% with etags enabled. E.g. if `http` can handle 50k req/s, then `woodland` should handle 43k req/s.
1. Clone repository from [GitHub](https://github.com/avoidwork/woodland).
1. Install dependencies with `npm` or `yarn`.
1. Execute `benchmark` script with `npm` or `yarn`.
Results with node.js 20.8.0 & an Intel i9-12900HX (mobile) on Windows 11, with etags disabled.
```console
> node benchmark.js
http
┌─────────┬──────┬──────┬───────┬───────┬──────────┬──────────┬───────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼──────┼──────┼───────┼───────┼──────────┼──────────┼───────┤
│ Latency │ 1 ms │ 8 ms │ 42 ms │ 47 ms │ 10.81 ms │ 10.26 ms │ 88 ms │
└─────────┴──────┴──────┴───────┴───────┴──────────┴──────────┴───────┘
┌───────────┬─────────┬─────────┬───────┬───────┬─────────┬─────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼───────┼───────┼─────────┼─────────┼─────────┤
│ Req/Sec │ 75967 │ 75967 │ 88703 │ 93823 │ 88409.6 │ 4152.76 │ 75952 │
├───────────┼─────────┼─────────┼───────┼───────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 15.4 MB │ 15.4 MB │ 18 MB │ 19 MB │ 17.9 MB │ 841 kB │ 15.4 MB │
└───────────┴─────────┴─────────┴───────┴───────┴─────────┴─────────┴─────────┘
woodland
┌─────────┬──────┬──────┬───────┬───────┬──────────┬──────────┬────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼──────┼──────┼───────┼───────┼──────────┼──────────┼────────┤
│ Latency │ 2 ms │ 9 ms │ 57 ms │ 67 ms │ 12.82 ms │ 13.04 ms │ 119 ms │
└─────────┴──────┴──────┴───────┴───────┴──────────┴──────────┴────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬──────────┬─────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼─────────┼─────────┼──────────┼─────────┼─────────┤
│ Req/Sec │ 66687 │ 66687 │ 75263 │ 76095 │ 75041.61 │ 1482.92 │ 66667 │
├───────────┼─────────┼─────────┼─────────┼─────────┼──────────┼─────────┼─────────┤
│ Bytes/Sec │ 14.1 MB │ 14.1 MB │ 15.9 MB │ 16.1 MB │ 15.8 MB │ 312 kB │ 14.1 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴──────────┴─────────┴─────────┘
```
## API
### constructor ({...})
Returns a woodland instance. Enable directory browsing & traversal with `autoindex`. Create an automatic `x-response-time` response header with `time` & `digit`. Customize `etag` response header with `seed`.
#### Configuration
```json
{
"autoindex": false,
"cacheSize": 1000,
"cacheTTL": 300000,
"charset": "utf-8",
"corsExpose": "",
"defaultHeaders": {},
"digit": 3,
"etags": true,
"indexes": [
"index.htm",
"index.html"
],
"logging": {
"enabled": true,
"format": "%h %l %u %t \"%r\" %>s %b",
"level": "info"
},
"origins": [
"*"
],
"silent": false,
"time": false
}
```
### allowed (method, uri, override = false)
Calls `routes()` and returns a `Boolean` to indicate if `method` is allowed for `uri`.
### allows (uri, override = false)
Returns a `String` for the `Allow` header. Caches value, & will update cache if `override` is `true`.
### always (path, fn)
Registers middleware for a route for all HTTP methods; runs first. `path` is a regular expression (as a string), and if not passed it defaults to `/.*`.
Execute `ignore(fn)` if you do not want the middleware included for calculating the `Allow` header.
### ignore (fn)
Ignores `fn` for calculating the return of `allows()`.
### decorate (req, res)
Decorates `allow, body, cors, host, ip, params, & parsed` on `req` and `error(status[, body, headers]), header(key, value), json(body[, status, headers]), locals{} & redirect(url[, perm = false])` on `res`.
### delete ([path = "/.*",] ...fn)
Registers middleware for a route. `path` is a regular expression (as a string), and if not passed it defaults to `/.*`.
### etag (...args)
Returns a String to be used as an etag response header value.
### files (root = "/", folder = process.cwd())
Serve static files on disk.
### get ([path = "/.*",] ...fn)
Registers middleware for a route. `path` is a regular expression (as a string), and if not passed it defaults to `/.*`. See `always()` if you want the middleware to be used for all HTTP methods.
### list (method = "get", type = "array")
Returns an `Array` or `Object` of routes for the specified method.
### log (msg = "", level = "debug")
Logs to `stdout` or `stderr` depending on the `level`, & what the minimum log level is set to.
### patch ([path = "/.*",] ...fn)
Registers middleware for a route. `path` is a regular expression (as a string), and if not passed it defaults to `/.*`.
### post ([path = "/.*",] ...fn)
Registers middleware for a route. `path` is a regular expression (as a string), and if not passed it defaults to `/.*`.
### put ([path = "/.*",] ...fn)
Registers middleware for a route. `path` is a regular expression (as a string), and if not passed it defaults to `/.*`.
### onDone (req, res, body, headers)
**Override** to customize final handler. Must terminate response.
### onReady (req, res, body, status, headers)
**Override** to customize response `body`, `status`, or `headers`. Must call `onSend()`.
### onSend (req, res, body, status, headers)
**Override** to customize response `body`, `status`, or `headers`. Must return `[body, status, headers]`!
### options ([path = "/.*",] ...fn)
Registers middleware for a route. `path` is a regular expression (as a string), and if not passed it defaults to `/.*`.
### route (req, res)
Function for `http.createServer()` or `https.createServer()`.
### routes (uri, method, override = false)
Returns an `Array` of middleware for the request. Caches value, & will update cache if `override` is `true`.
### stream (req, res, file = {charset: "", etag: "", path: "", stats: {mtime: datetime, size: int}})
Streams a file to the client.
### use ([path = "/.*",] ...fn[, method = "GET"])
Registers middleware for a route. `path` is a regular expression (as a string), and if not passed it defaults to `/.*`. See `always()` if you want the middleware to be used for all HTTP methods.
All HTTP methods are available on the prototype (partial application of the third argument), e.g. `get([path,] ...fn)` & `options([path,] ...fn)`.
## Command Line Interface (CLI)
When woodland is installed as a global module you can serve the contents of a folder by executing `woodland` in a shell. Optional parameters are `--ip=127.0.0.1` & `--port=8000`.
```console
Node.js v20.8.0
PS C:\Users\jason\Projects> npm install -g woodland
changed 6 packages in 1s
PS C:\Users\jason\Projects> woodland
id=woodland, hostname=localhost, ip=127.0.0.1, port=8000
127.0.0.1 - [7/Oct/2023:15:18:18 -0400] "GET / HTTP/1.1" 200 1327
127.0.0.1 - [7/Oct/2023:15:18:26 -0400] "GET /woodland/ HTTP/1.1" 200 2167
127.0.0.1 - [7/Oct/2023:15:18:29 -0400] "GET /woodland/dist/ HTTP/1.1" 200 913
127.0.0.1 - [7/Oct/2023:15:18:32 -0400] "GET /woodland/dist/woodland.js HTTP/1.1" 200 26385
127.0.0.1 - [7/Oct/2023:15:18:47 -0400] "GET /woodland/benchmark.js HTTP/1.1" 200 1657
127.0.0.1 - [7/Oct/2023:15:18:58 -0400] "GET /woodland/sample.js HTTP/1.1" 200 845
127.0.0.1 - [7/Oct/2023:15:19:07 -0400] "GET /woodland/sample.js HTTP/1.1" 304 0
```
## Event Handlers
Event Emitter syntax for the following events:
### connect (req, res)
Executes after the connection has been decorated, but before the middleware executes.
```javascript
app.on("connect", (req, res) => res.header("x-custom-header", "abc-def"));
```
### error (req, res, err)
Executes after the response has been sent.
```javascript
app.on("error", (req, res, err) => { /* log err */ });
```
### finish (req, res)
Executes after the response has been sent.
```javascript
app.on("finish", (req, res) => { /* telemetry */ });
```
### stream (req, res)
Executes after the response has been streamed.
```javascript
app.on("stream", (req, res, err) => { /* telemetry */ });
```
## Helpers
`req` & `res` are decorated with helper functions to simplify responding.
### req.exit()
Exit the middleware chain if the route is un-protected.
### res.error(status[, body, headers])
Sends an error response.
### res.header(key, value)
Shorthand of `res.setHeader()`.
### res.json(body, [status = 200, headers])
Sends a JSON response.
### res.redirect(uri[, perm = false])
Sends a redirection response.
### res.send(body, [status = 200, headers = {}])
Sends a response. `Range` header is ignored on `stream` responses.
### res.set(headers = {})
Shorthand of `res.setHeaders()` which accepts `Object`, `Map`, or `Headers` instances.
### res.status(arg)
Sets the response `statusCode` property.
## Logging
Woodland defaults to [Common Log Format](https://en.wikipedia.org/wiki/Common_Log_Format) but supports [Common Log Format with Virtual Host](https://httpd.apache.org/docs/trunk/mod/mod_log_config.html), & [NCSA extended/combined log format](https://httpd.apache.org/docs/trunk/mod/mod_log_config.html) with an `info` level by default. You can change the `stdout` output by changing `logging.format` with valid placeholders.
You can disable woodland's logging by configuration with `{logging: {enabled: false}}`.
## License
Copyright (c) 2024 Jason Mulligan
Licensed under the BSD-3 license.