Build an HTTP server out of composable blocks
// server.mjs
export default function(request) {
console.log(request)
return {
status: 200,
headers: {
'Content-Type': 'text/plain'
},
body: 'Hello World!'
}
}
yarn pass-notes server.mjs
Handle HTTP requests with a function that takes request data and returns response data. Compose your functions with pre-built functions to quickly implement complex workflows.
Install passing-notes by running
yarn add passing-notes
passing-notes
provides an interface for building HTTP servers. At its core,
it takes a function that takes in request data and returns response data.
// server.mjs
export default function(request) {
// request = {
// version: '2.0',
// method: 'GET',
// url: '/',
// headers: {},
// body: ''
// }
return {
status: 200,
headers: {
'content-type': 'text/plain'
},
body: 'Hello World!'
}
}
This code can be run either from the command line:
yarn pass-notes server.mjs
Or from JavaScript:
import {startServer} from 'passing-notes'
import handleRequest from './server.mjs'
startServer({port: 8080}, handleRequest)
Taking cues from popular tools like Express, we encourage organizing your request-handling logic into middleware:
import {compose} from 'passing-notes'
export default compose(
(next) => (request) => {
const response = next(request)
return {
...response,
headers: {
...response.headers,
'content-type': 'application/json'
},
body: JSON.stringify(response.body)
}
},
(next) => (request) => {
return {
status: 200,
headers: {},
body: {
message: 'A serializable object'
}
}
},
() => () => ({ status: 404 })
)
Each request is passed from top to bottom until one of the middleware returns a response. That response then moves up and is ultimately sent to the client. In this way, each middleware is given a chance to process and modify the request and response data.
Note that one of the middleware must return a response, otherwise, an Error
is
thrown and translated into a 500
response.
We've built and packaged some middleware that handle common use cases:
static
: Serves static files from the file systemui
: Serves application code to the browserrpc
: Simple communication between browser and serverwebsocket
: Accept WebSocket connectionssse
: Send and receive server-sent events
When using the pass-notes
CLI tool, during development (when
NODE_ENV !== 'production'
), additional features are provided:
The provided module and its dependencies are watched for changes and re-imported before each request. Changes to your code automatically take effect without you needing to restart the process.
The node_modules
directory, however, is not monitored due to its size.
HTTPS is automatically supported for localhost
with a self-signed certificate.
This is needed for browsers to use HTTP/2.0 when making requests to the server.
A common pattern for implementing per-environment configuration is to store that configuration in a file that is modified per environment. This is useful for scenarios for which it's not convenient to directly set environment variables.
We support setting environment variables via a .env.yml
file:
FOO: string
BAR:
- JSON
- array
BAZ:
key1: JSON
key2: object
By default, the method and URL of each request and the status of the response is
logged to STDOUT
, alongside a timestamp and how long it took to return the
response.
To log additional information:
import {Logger} from 'passing-notes'
export const logger = new Logger()
export default function(request) {
logger.log({
level: 'INFO',
topic: 'App',
message: 'A user did a thing'
})
// ...
}
In addition, our Logger
provides a way to log the runtime for expensive tasks,
like database queries:
const finish = logger.measure({
level: 'INFO',
topic: 'DB',
message: 'Starting DB Query'
})
// Perform DB Query
finish({
message: 'Finished'
})
The logger can be passed to any middleware that needs it as an argument.
A CLI that takes an ES module that exports an HTTP request handler and uses it to start an HTTP server.
// server.mjs
export default function(request) {
console.log(request)
return {
status: 200,
headers: {
'Content-Type': 'text/plain'
},
body: 'Hello World!'
}
}
yarn pass-notes server.mjs
The ES module's default export must be a function that takes as argument an object with the following keys:
version
: The HTTP version used, either'1.1'
or'2.0'
method
: An HTTP request method in capital letters (e.g.GET
orPOST
)url
: The absolute URL or path to a resourceheaders
: An object mapping case-insensitive HTTP header names to valuesbody
: The HTTP request body as a string or buffer
And returns an object (or Promise
resolving to an object) with the following
keys:
status
: The HTTP response status code (e.g.200
)headers
body
push
: An optional array of requests that will be fed back into the request handler to compute responses and then pushed to the client. This is only supported over HTTP/2 (indicated byrequest.version
being'2.0'
).
The request handler is also able to negotiate protocol upgrades (e.g. to
WebSocket). When a client sends the Connection: Upgrade
header, the request
handler can respond with status 101 Switching Protocols
and immediately take
control of the underlying TCP Socket
by providing an upgrade
method on the
response object.
import {createHash} from 'node:crypto'
import WebSocket from 'ws'
const webSocketHashingConstant = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
export default function(request) {
if (
request.headers.connection === 'Upgrade' &&
request.headers.upgrade === 'websocket'
) {
const key = request.headers['sec-websocket-key']
return {
status: 101,
headers: {
Upgrade: 'websocket',
Connection: 'Upgrade',
'Sec-WebSocket-Accept': createHash('sha1')
.update(`${key}${webSocketHashingConstant}`)
.digest('base64'),
},
async upgrade(socket, head) {
const ws = new WebSocket(null)
ws.setSocket(socket, head, {
maxPayload: 100 * 1024 * 1024,
skipUTF8Validation: false,
})
ws.on('message', (message) => {
console.log(message.toString())
})
},
}
} else {
return {
status: 426,
}
}
}
See Pre-Built Middleware for pre-packaged solutions.
This HTTP server supports HTTP/1.1 and HTTP/2 as well as TLS.
A self-signed certificate is automatically generated for localhost
when
NODE_ENV
is not set to production
. Otherwise, a certificate can be provided
by exporting an object named tls
containing any of the options for
tls.createSecureContext
,
for example:
export const tls = {
cert: 'PEM format string',
key: 'PEM format string'
}
CERT
and KEY
can also be provided as environment variables.
When NODE_ENV
is not set to production
, the provided ES module is
re-imported whenever it or its dependencies change. Note that node_modules
are
never re-imported.
By default, the method and URL for every request is logged to STDOUT.
In order to log additional events to STDOUT, a custom logger can be created and exported:
import {Logger} from 'passing-notes'
export const logger = new Logger()
This logger is expected to provide the following interface:
- It extends
EventEmitter
- It emits
log
events with two arguments:event
: An object containing:time
: A UNIX timestamplevel
: One ofTRACE
,DEBUG
,INFO
,WARN
,ERROR
, orFATAL
topic
: A string that categorizes the log eventmessage
: A description of the log eventduration
: An optional millisecond durationerror
: An optionalError
object to print
logLine
a formatted string to print to STDOUT
log(event)
: Computes a timestamp and emits alog
event.measure(event)
: Logs the start of a task. Returns a function that when called, computes the duration and logs the end of the task.