merry
Features
- fast: using Node streams, merry handles request like no other
- fun: helps with boring stuff like error handling
- communicative: standardized ndjson logs for everything
- sincere: doesn't monkey patch Node's built-ins
- linear: smooth sailing from tinkering to production
- very cute:
๐ ๐ โต๏ธ ๐ ๐
Usage
Given the following index.js
:
const listen = require('merry/listen')
const string = require('merry/string')
const notFound = require('merry/404')
const error = require('merry/error')
const Env = require('merry/env')
const merry = require('merry')
const env = Env({ PORT: 8080 })
const app = merry()
app.router({ default: '/404' }, [
[ '/', (req, res, params, done) => {
done(null, string('hello world'))
}],
[ '/error', (req, res, params, done) => {
done(error(500, 'server error!'))
}],
['/api', {
get: (req, res, params, done) => {
done(null, string('hello very explicit GET'))
}
}],
[ '/404', notFound() ]
])
const handler = app.start()
listen(env.PORT, handler)
Run using:
$ node index.js | merry-pretty
Logging
Merry uses the bole
logger under the hood. When you create a new merry
app,
we enable a log forwarder that by default prints all logs to process.stdout
.
To send a log, we must first create an instance of the logger. This is done by
requireing the merry/log
file, and instantiating it with a name. The name is
used to help determine where the log was sent from, which is very helpful when
debugging applications:
const Log = require('merry/log')
const log = Log('some-filename')
log.inf('logging!')
There are different log levels that can be used. The possible log levels are:
- debug: used for developer annotation only, should not be enable in production
- info: used for transactional messages
- warn: used for expected errors
- error: used for unexpected (critical) errors
const Log = require('merry/log')
const log = Log('my-file-name')
log.debug('it works!')
log.info('hey')
log.warn('oh')
log.error('oh no!')
The difference between an expected and unexpected error is that the first is generally caused by a user (e.g. wrong password) and the system knows how to respond, and the latter is caused by the system (e.g. there's no database) and the system doesn't know how to handle it.
Error handling
The send(err, stream)
callback can either take an error or a stream. If an
error has .statusCode
property, that value will be used for res.statusCode
.
Else it'll use any status code that was set previously, and default to 500
.
'info'
. It's important to
not disclose any internal information in 4xx
type errors, as it can lead to
serious security vulnerabilities. All errors in other ranges (typically 5xx
)
will send back the message 'server error'
and are logged as loglevel
'error'
.
Configuration
Generally there are two ways of passing configuration into an application. Through files and through command line arguments. In practice it turns out passing environment variables can be done with less friction than using files. Especially in siloed environments such as Docker and Kubernetes where mounting volumes can at times be tricky, but passing environment variables is trivial.
Merry ships with an environment argument validator that checks the type of
argument passed in, and optionally falls back to a default if no value is
passed in. To set the (very common) $PORT
variable to default to 8080
do:
const Env = require('merry/env')
const env = Env({ PORT: 8080 })
console.log('port: ' + env.PORT)
And then from the CLI do:
node ./server.js
// => port: 8080
PORT=1234 node ./server.js
// => port: 1234
JSON
If Object
and Array
are the data primitives of JavaScript, JSON is the
primitive of APIs. To help create JSON there's merry/json
. It sets the right
headers on res
and efficiently turns JavaScript to JSON:
const json = require('merry/json')
const merry = require('merry')
const http = require('http')
const app = merry()
app.router(['/', (req, res, params, done) => {
done(null, json(req, res, { message: 'hello JSON' }))
}])
http.createServer(app.start()).listen(8080)
Routing
Merry uses server-router
under the hood to create its routes. Routes are
created using recursive arrays that are turned into an efficient trie
structure under the hood. You don't need to worry about any of this though; all
you need to know is that we've tested it and it's probably among the fastest
methods out there. Routes look like this:
const merry = require('merry')
const app = merry()
app.router([
['/', handleIndex],
['/foo', handleFoo, [
['/:bar', handleFoobarPartial]
]]
])
Partial routes can be set using the ':'
delimiter. Any route that's
registered in this was will be passed to the params
argument as a key. So
given a route of /foo/:bar
and we call it with /foo/hello
, it will show up
in params
as { bar: 'hello' }
.
API
app = merry(opts)
Create a new instance of merry
. Takes optional opts:
- opts.logLevel: defaults to
'info'
. Determine the cutoff point for logging. - opts.logStream: defaults to
process.stdout
. Set the output stream to write logs to
app.router(opts?, [routes])
Register routes on the router. Take the following opts:
- default: (default:
'/404'
) Default route handler if no route matches
routes
Each route has a signature of (req, res, params, done)
:
- req: the server's unmodified
req
object - res: the server's unmodified
res
object - params: the parameters picked up from the
router
using the:route
syntax in the route - done: a handler with a signature of
(err, stream)
, that takes either an error or a stream. If an error is passed it sets a statusCode of500
and prints out the error tostdout
and sends a'server error'
reply. If a stream is passed it pipes the stream tores
until it is done.
handler = app.start()
Create a handler that can be passed directly into an http
server.
const string = require('merry/string')
const merry = require('merry')
const http = require('http')
const app = merry()
app.router(['/', handleRoute])
const handler = app.start()
http.createHttpServer(handler).listen(8080)
function handleRoute (req, res, params, done) {
done(null, string('hello planet'))
}
string = merry/string(string)
Create a readableStream
from a string. Uses from2-string
under the hood
json = merry/json(req, res, object)
Create a readableStream
from an object. req
and res
must be passed in to
set the appropriate headers. Uses from2-string
under the hood
error = merry/error(statusCode, message, err?)
Create an HTTP error with a statusCode and a message. By passing an erorr as
the third argument it will wrap the error using explain-error
to keep prior
stack traces.
notFound = merry/404()
Create a naive /404
handler that can be passed into a path.
log = merry/log(name)
Create a new log client that forwards logs to the main app
. See the logging
section for more details.
log = merry/env(settings)
Create a new configuration client that reads environment variables from
process.env
and validates them against configuration.
Installation
$ npm install merry