README
Litespeed
- Installation
- Example
- Configuration
- Routes Config
- Handlers
- PreHandlers
- Router
- Validation
- Errors
- Logging
- Server API
- Plugins
Litespeed is a micro web framework for building APIs in Node.js. Based on configuration and promises, it keeps things fast, simple, predictable, and is a breeze to get started. It comes with built-in input validation, a routing library, an error library, support for pre-handlers (aka middleware), and more. A perfect solution for microservices!
Why another Node framework?
There are a lot of great frameworks out there such as Express, Restify, Koa, and Hapi. They have been around for awhile, and each have their place in the Javascript ecosystem. But a lot of the time, they can be overkill for your app, especially if you need something simple but extendable for a microservice API. Litespeed is a lightweight approach that takes ideas from other frameworks and brings them together in a compact and easy to understand way. It's also nice to have built-in validation, error handling, and logging ;)
Installation
$ npm install litespeed --save
# or to use the CLI
$ npm install litespeed -g
Example
const Litespeed = require('litespeed')
new Litespeed({/* configuration */}).route({
method: 'GET',
url: '/hello/:name',
handler: (req) => {
return { message: `Hello, ${req.params.name}!` }
}
}).start()
That's it! Run this code and go to http://localhost:8000/hello/jason
. You should be greeted with {"message":"Hello, jason!"}
.
Configuration
There are several config options, all of which have optimal values by default. But they can be easily changed by passing an object to the Litespeed instance (see the example above).
name
The name of the server, set in theServer
response header. Can be set tofalse
to omit the header entirely.Type: string, boolean
Default:Litespeed
host
The host to run the server on. Uses theHOST
environment variable if it exists.Type: string
Default:process.env.HOST || '0.0.0.0'
port
The port to run the server on. Uses thePORT
environment variable if it exists.Type: number
Default:process.env.PORT || 8000
trailingSlash
Whether to accept a URL both with and without a trailing slash. Iftrue
, the trailing slash is ignored and the route is still found. Iffalse
, the route will return a 404 with a trailing slash.Type: boolean
Default:true
pretty
Whether to prettify the response JSON.Type: boolean
Default:false
ortrue if NODE_ENV starts with "dev"
timeout
The amount of time in milliseconds to wait before a request is timed out, returning a 408 error to the client.Type: number
Default:5000
(5s)payloadLimit
The max size (in bytes) a request payload can be. This helps prevents malicious use by client's sending giant payloads to your API. Will return a 413 error if exceeded.Type: number
Default:1048576
(1mb)stripUnknown
Whether to take out unknown values from the request payload and URL query data. This only takes effect if validation is running on the endpoint, and will throw away any value not included in the validation object.Type: boolean
Default:true
protective
Whether to add basic security headers on the response and require a user-agent. Will currently addX-Content-Type-Options: nosniff
andX-Frame-Options: deny
, and send a 403 error if noUser-Agent
header is present on the request.Type: boolean
Default:true
realIp
Whether to check for an IP address in theX-Forwarded-For
header, taking the first address from the comma-separated list. This helps get the client's real IP address if the request is coming through a proxy server. Will fallback to the actual IP address if the header is not found.Type: boolean
Default:true
logs
Tag names of the loggers to enable. See Logging for more info on the tags available. Can also be set tofalse
to disable all logging.Type: array, boolean
Default: alllogTimestamp
Whether to log the current timestamp with each message.Type: boolean
Default:true
logColors
Whether to use CLI logColors when logging.Type: boolean
Default:true
preHandlers
Global functions to run for every route in the app. See PreHandlers for more info.Type: array
Default:[]
Routes Config
A route is defined by a simple object with the following configuration.
method
The method to use for the route. This is usually one ofHEAD, GET, POST, PUT, PATCH, DELETE
if using the HTTP protocol, but can be set to anything if you're using a custom protocol.Type: string
Requiredurl
The endpoint of the route. This can include segments or be a regular expression. See Router for details.Type: string, regexp
RequiredstatusCode
The HTTP status code to return on a successful response.Type: number
Default:200
validate
The validations to run against the request. See Validation for more info.Type: object
preHandlers
The preHandler functions to run before the route's handler. See PreHandlers for more info.Type: array
handler
The request handler function. See Handlers for more info.Type: function
RequiredonError
A custom function to run when an error occurs (overwrites default error handling). See Errors for more info. Note: if this is being used, you are responsible for your own error logging!Type: function
HTTP OPTIONS
The HTTP OPTIONS request is handled automatically for each URL. If requested, the server will respond with an empty body and the Allow
header with each method supported for that particular URL.
Handlers
Route handlers are functions on the route config that handle sending responses to the client. To respond from the handler, you can either return a value or return a Promise. The function has two parameters passed to it: the request object and the response object.
server.route({
// ...
handler: (request, response) => {
return 'welcome!'
}
})
request
The request parameter is an object that contains information about the client request.
body
The payload data from the request body.query
The query data from the request URL.params
The param data from the request URL. See Router for more info.headers
The request headers.context
The context data passed through the preHandlers. See Plugins for more info.info
Metadata about the request.remoteAddress
The client's IP address
response
Unlike other Node web frameworks, the response parameter is not used to actually send the response itself--that is done by returning from the handler (with the exception of .redirect
). Instead, this object contains helper methods to mutate the response before it is sent.
.pass(name, value)
Passes context between preHandlers (see PreHandlers for more info). Context data is accessed in the handler withrequest.context.*
..setHeader(name, value)
Sets a response header..removeHeader(name, value)
Removes a response header..redirect(url, code)
Sends a redirect response to the client.code
defaults to301
if not set. There is no need to return anything from the handler after this is called.
Async/Await
Since handlers are based on Promises, you can easily use ES7's async/await feature. Any errors thrown within the handler are caught by Litespeed's error handler and outputted correctly. This removes the need to have a bunch of try/catch blocks (though you can still have them if you need to). Errors within sub-promises (such as the createUser
function in the example below) will bubble up to the handler and outputted the same way.
server.route({
method: 'POST',
url: '/signup',
statusCode: 201,
handler: async (req) => {
const user = await createUser(req.body)
return { user }
}
})
Note: If using Node >=6, all you need for async/await support is transform-async-to-generator rather than an entire preset.
PreHandlers
Route preHandlers follow the exact same structure as Handlers, but you can pass an array of them to the routes config as well as the server config. Note: they are guaranteed to run in order, making it possible to rely on context from other preHandlers!
Router
Litespeed comes with a built-in router that supports segment parameters and regular expressions. This provides a quick and flexible way to define your endpoints. Note: You cannot have two routes defined with the same method and URL or a startup error will be thrown.
Segments
Segments are named sections of the URL prefixed with :
, and they can pass values to the handler. You can have as many segments as you want in your URL.
server.route({
// ...
url: '/welcome/:name',
handler: (request) => {
// if requested with /welcome/jason
// expect request.params.name to equal 'jason'
}
})
Regular Expressions
The router also supports regular expressions, including their capture groups for getting data. If you define a URL as a regex with a capture group (a piece wrapped in parentheses), you can access that data by its regex index ($1
, $2
, etc.)
Note: if using a regex as a URL, make sure you either use a regular expression literal or new RegExp()
. Read this if you're new to regular expressions in Javascript.
server.route({
// ...
url: /^\/welcome\/(.*)$/,
handler: (request) => {
// if requested with /welcome/jason
// expect request.params.$1 to equal 'jason'
}
})
Validation
A validate
object can be used on the routes config to run validations against the request body, query, and params. Litespeed comes with a list of predefined validations, which can be chained together. See the example below on how to use. If stripUnknown
is enabled on the server, any values not in the validate
object will be throw away.
The response is not sent after one failed validation. Litespeed will continue through the validations and display all errors in the response rather than just the first one. However, only one error from each validation chain is thrown at a time.
const { Validator } = require('litespeed')
server.route({
url: '/resource/:id'
validate: {
params: {
id: new Validator().isUUID()
},
body: {
name: new Validator().required(),
email: new Validator().required().isEmail()
}
}
})
Validation errors will look like this in the response:
{
"statusCode": 400,
"error": "Bad Request",
"message": "A validation error occurred",
"validation": [
{ "field": "id", "message": "...", "where": "params" },
{ "field": "name", "message": "...", "where": "body" }
]
}
Predefined Validations
.required()
.contains(string)
.equals(compare)
.isAfter(date)
.isAlpha(locale)
.isAlphanumeric(locale)
.isAscii()
.isBase64()
.isBefore(date)
.isBoolean()
.isCurrency(opts)
.isDataURI()
.isDate()
.isDivisibleBy(num)
.isEmail(opts)
.isFQDN(opts)
.isIpAddress(ver)
.isEnum(values)
.isNumber(opts = {})
.isJSON()
.isLength(opts)
.isLowercase()
.isMACAddress()
.isPhoneNumber(locale)
.isMongoId()
.isURL(opts)
.isUUID(ver)
.isUppercase()
.isWhitelisted(chars)
.matches(pattern, mods)
Most of these validations use validator.js, so visit the Readme in that repo for further details.
Errors
Any errors occurring throughout the application or in an async/await function will be automatically logged and formatted for the response. Errors can occur in a few different ways:
- By
throw
ing anywhere in the app - Runtime code errors
- Returning an error from a handler
- An unhandled promise rejection
Error responses are simply objects with the following structure:
statusCode
The status code of the error (uses500
for unknown errors)error
The error type that occurredmessage
A descriptive error message for the client
If a Javascript Error is thrown, a 500 Internal Server Error
will be returned, and if in dev mode (meaning NODE_ENV
starts with dev
), the error message will be sent to the response as well for easy debugging. All server errors (meaning statusCode is >= 500) will be logged to the console with a description and a stack trace.
Litespeed comes with a list of predefined errors that accept custom messages (set as the message
key in the response). See the example below on how to use them.
const { Errors } = require('litespeed')
server.route({
//...
handler: async () => {
const resource = getNonExistantResource()
if (!resource) {
throw new Errors().notFound('resource does not exist')
}
return resource
}
})
{
"statusCode": 404,
"error":"Not Found",
"message":"resource does not exist"
}
Predefined Errors
400
.badRequest()
401
.unauthorized()
402
.paymentRequired()
403
.forbidden()
404
.notFound()
405
.methodNotAllowed()
406
.notAcceptable()
407
.proxyAuthRequired()
408
.requestTimeout()
409
.conflict()
410
.gone()
411
.lengthRequired()
412
.preconditionFailed()
413
.payloadTooLarge()
414
.uriTooLong()
415
.unsupportedMediaType()
416
.rangeNotSatisfiable()
417
.expectationFailed()
422
.unprocessableEntity()
423
.locked()
424
.failedDependency()
426
.upgradeRequired()
428
.preconditionRequired()
429
.tooManyRequests()
431
.headersTooLarge()
500
.internal()
501
.notImplemented()
502
.badGateway()
503
.serviceUnavailable()
504
.gatewayTimeout()
505
.httpVersionNotSupported()
507
.insufficientStorage()
508
.loopDetected()
509
.bandwidthLimitExceeded()
510
.notExtended()
511
.networkAuthRequired()
Logging
By default, all Litespeed logging is turned on. This can be modified by specifying log tags in the server config. Each line outputted to the console includes the server name, a timestamp, and the message. Here are the available log tags:
server
Outputs a message when the server starts with its URL.request
Outputs the method, URL, IP address, and response code when a client makes a request.error
Outputs any server errors, meaning any error with a statusCode >= 500 (which includes runtime errors). Logged errors will include a message, and a stack trace.
Colors in the output can be turned off if they are causing problems by specifying logColors: false
in the server config. Note: if you specify any log tags, the default tags will be overwritten. So whatever you specify will be the only active tags.
Server API
The Litespeed server is a class that must be instantiated with the new
keyword, and can be passed an optional object of configuration options.
const server = new Litespeed({/* configuration */})
server.start(callback)
Starts the server on the host and port defined in the instantiation. This will return a promise or you can use a callback, both of which pass the URL of the server.
/* with a callback */
server.start((url) => console.log(`Running at ${url}`))
/* or with a promise */
server.start().then((url) => console.log(`Running at ${url}`))
server.route(config)
Creates a new route. Can be a single route config, or an array of configs for creating multiple routes at once. See Routes Config for structure.
/* for a single route */
server.route({/* route config */})
/* for many routes */
server.route([
{/* route config */},
{/* route config */}
])
server.routes(config)
You can use this to scan a directory and pull in routes from many files. Prevents having to manually require and create all your routes. Takes a config object with the following structure.
dir
A glob pattern to search for.Type: string
Default:routes/**/*.js
cwd
The current working directory to use.Type: string
Default:process.cwd()
server.routes({
dir: 'routes/**/*.js',
cwd: __dirname
})
server.inject(config)
Lets you inject API requests without the overhead of starting the server. This works great for running integration tests on your endpoints. Takes an object with a method
and url
, as well as optional headers
and body
. Returns a Promise resolving with the response.
/* define the route */
server.route({
method: 'POST',
url: '/test',
handler: (req) => req.body.name
})
/* test the route */
server.inject({
method: 'POST',
url: '/test',
body: { name: 'Jason' }
}).then((res) => {
// expect res to equal 'Jason'
})
Plugins
Litespeed easily supports third-party plugins through the use of PreHandlers. To create a plugin, simply create a preHandler that exports a function returning a Promise (if async). Then it can be set as a global preHandler in the app using it.
You can also pass context from your plugin to other plugins or to the route handler by using response.pass(name, value)
(see response). These values are then accessed with request.context.*
.
Popular Plugins
Contributing
If you come across an issue or have a feature idea, don't hesitate to create a pull request/issue to discuss it.