@butlerlogic/common-api
v1.6.0
Published
An API engineering productivity kit for Express.
Downloads
43
Readme
Common API Utilities
Like this project? Let people know with a tweet.
This is a lightweight library containing a few commonly used methods for creating API's with Express.js. It is ideally suited for prototyping API's.
There's a short walk-thru/guide/example available on Quora
Installation:
npm install @butlerlogic/common-api -S
Usage
const express = require('express')
const API = require('@butlerlogic/common-api')
const app = express()
app.post('/endpoint', API.validateJsonBody(), (req, res) => { ... })
const server = app.listen(() => console.log('Server is running.'))
Shortcuts
Request Middleware
- log()
- logRequestHeaders()
- logRedirects([bool])
- litmusTest([message]))
- validateJsonBody
- validNumericId
- validId
- validResult(res, callback)
- basicauth(user, password)
- bearer(token)
- applyCommonConfiguration(app, [autolog])
- applySimpleCORS(app, host='*')
- allowHeaders('Origin', 'X-Requested-With')
- allowMethods('GET', 'POST', 'OPTIONS')
- allowOrigins('a.domain.com', 'b.domain.com')
- allowPreflight()
- allowAll('host')
Responses
- 200
- OK
- 201
- CREATED
- 401
- UNAUTHORIZED
- 404
- NOT_FOUND
- 501
- NOT_IMPLEMENTED
- Other HTTP Status Codes
- redirect(url, [permanent, moved])
- reply(anything)
- replyWithError(res, [status, message]|error)
- replyWithMaskedError(res, [status, message]|error)
Utilities
- createUUID
- atob(value)
- applyBaseUrl (req, route = '/' [, forceTLS = false])
- applyRelativeUrl (req, route = '/' [, forceTLS = false])
- errorType
- commonHeaders
- httpMethods
Middleware
The following static methods are available:
log
Configures a simple console logging utility (with colorized output). This can be used as middleware for all requests or individual requests.
app.use(API.log)
app.post('/endpoint', API.log, ...)
logRequestHeaders
Configures a simple console logging utility (with colorized output), which will log the request headers of the request. This can be used as middleware for all requests or individual requests.
app.use(API.logRequestHeaders)
app.post('/endpoint', API.logRequestHeaders, ...)
logRedirects([bool])
Toggle redirect logging. This is not middleware. It is just a function to determine whether redirected requests should be logged to the console or not.
API.logRedirects() // Defaults to `true`
API.logRedirects(true)
API.logRedirects(false)
When active, this will log a message like:
Redirect GET /redirect → http://domain.com/endpoint
litmusTest([message])
This pass-thru middleware component is useful for determining whether a route or responder is reachable or not. A message (LITMUS TEST
by default) is logged to the console/stdout, without affecting the network request/response.
app.use('/endpoint', API.litmusTest('endpoint reachable'), ...)
validateJsonBody
app.post('/endpoint', API.validateJsonBody(), ...)
Validates a request body exists and consists of valid JSON.
It is also possible to verify that the JSON body contains specific "top level" attributes (i.e. nesting is not supported).
For example,
app.post('/endpoint', API.validateJsonBody('a', 'b', 'c'), ...)
The code above will verify that the request body is valid JSON containing attributes a
, b
, and c
.
Valid JSON
{
"a": "text",
"b": "text",
"c": "text",
"d": "extra is ok"
}
Invalid JSON
{
"a": "text",
"c": "text",
"d": "extra is ok"
}
Produces:
400 - Missing parameters: b
validNumericId
app.post('/endpoint/:id', API.validNumericId(), ...)
Assures :id
is a valid numeric value. This also supports a query parameter, such as /endpoint?id=12345
. This will add an attribute to the request
object (req.id
).
An alternative argument name can be provided, such as:
app.post('/endpoint/:userid', API.validNumericId('userid'), ...)
validId
app.post('/endpoint/:id', API.validId(), ...)
Assures :id
exists, as a string. This also supports a query parameter, such as /endpoint?id=some_id
. This will add an attribute to the request
object (req.id
).
An alternative argument name can be provided, such as:
app.post('/endpoint/:userid', API.validId('userid'), ...)
validResult(res, callback)
Inspects the result and returns a function that will throw an error or return results.
let checkResult = API.validResult(res, results => res.send(results))
app.get('/endpoint', (req, res) => { ...processing... }, checkResult)
basicauth(user, password)
This method will perform basic authentication. It will compare the authentication header credentials with the username and password.
For example, basicauth('user', 'passwd')
would compare the
user-submitted username/password to user
and passwd
. If
they do not match, a 401 (Not Authorized) response is sent.
If authentication is successful, a user
attribute will be
appended to the request (i.e. req.user
).
app.get('/secure', API.basicauth('user', 'passwd'), (req, res) => ...)
// req.user would be set "user" when authentication succeeds.
It is also possible to perform a more advanced authentication using a custom function. For example:
app.get('/secure', API.basicauth(function (username, password, grantedFn, deniedFn) {
if (confirmWithDatabase(username, password)) {
grantedFn()
} else {
deniedFn()
}
}))
The username
/password
will be supplied in plain text. The
grantedFn()
should be run when user authentication succeeds,
and the deniedFn()
should be run when it fails. Any downstream
middleware or other handlers will be able to access the username
by referencing req.user
.
bearer(token)
This method looks for a bearer token in the Authorization
request header. If the token does not match, a 401 (Unauthorized)
status is returned.
app.get('/secure/path', API.bearer('mytoken'), API.reply('authenticated'))
The code above would succeed for requests which contain the following request header:
Authorization: Bearer mytoken
The case-insensitive keyword "bearer" is required for this to work.
It is also possible to use a custom function to evaluate the request token. The function must by synchronous and return a boolean value (true
or false
).
app.get('/secure/path', API.bearer(function (token) {
return isValidToken(token)
}), API.reply('authenticated'))
Tokens do not necessarily represent a unique user, but they are often used
to lookup a user. To facilitate this, the req.user
attribute is set to the
value of the token so downstream middleware can perform lookups or further
validate the token.
applyCommonConfiguration(app, [autolog])
const express = require('express')
const app = express()
const API = require('@butlerlogic/common-api')
API.applyCommonConfiguration(app)
This helper method is designed to rapidly implement common endpoints. This can be used throughout the testing phase or in production.
The common configuration consists of 3 basic endpoints:
/ping
: A simple responder that returns a200 (OK)
response./version
: Responds with a plaintext body containing the version of the API, as determined by theversion
attribute found in thepackage.json
file of the server./info
: Responds with a JSON payload containing 3 attributes:runningSince
: The timestamp when the API server was launched.version
: Same as/version
above.routes
: An array of all known routes/endpoints of the API.
This also disables the x-powered-by
header used in Express.
By default, this method enables logging (using the log method). This can be turned off by passing false
as a second argument:
API.applyCommonConfiguration(app, false)
applySimpleCORS(app, host='*')
const express = require('express')
const app = express()
const API = require('@butlerlogic/common-api')
API.applySimpleCORS(app)
// API.applySimpleCORS(app, 'localhost')
Implementing CORS support while prototyping/developing an API can consume more time than most people anticipate. This method applies a simple CORS configuration so you can "continue coding". It is unlikely this configuration will be used in production environments unless the API is behind a secure gateway, but it helps temporarily resolve the most common challenges of developing with CORS.
This method applies 3 response headers to all responses:
Access-Control-Allow-Origin
: By default, this is set to*
, but the host can be modified by passing an optional 2nd argument to the function.Access-Control-Allow-Headers
: Set to'Origin, X-Requested-With, Content-Type, Accept'
Access-Control-Allow-Methods
: Set toGET, POST, PUT, PATCH, DELETE, OPTIONS
allowHeaders('Origin', 'X-Requested-With')
This CORS middleware feature can be used to specify/override which HTTP headers are allowed to be sent with requests to the server (by endpoint). This automatically handles setting the appopriate Access-Control-Allow-Headers
HTTP header.
const express = require('express')
const app = express()
const API = require('@butlerlogic/common-api')
API.applySimpleCORS(app)
app.get('/special/endpoint, API.allowOrigins('a.domain.com', 'b.domain.com'), (req, res) => {...})
This can also be applied to all requests:
app.use(API.allowOrigins('a.domain.com', 'b.domain.com'))
allowMethods('GET', 'POST', 'OPTIONS')
This CORS middleware feature can be used to specify/override which methods are allowed to be used when making HTTP requests to a specific endpoint/route. This automatically handles setting the appopriate Access-Control-Allow-Methods
HTTP header.
const express = require('express')
const app = express()
const API = require('@butlerlogic/common-api')
API.applySimpleCORS(app)
app.get('/special/endpoint, API.allowMethods('PATCH', 'POST'), (req, res) => {...})
This can also be applied to all requests:
app.use(API.allowMethods('GET'))
allowOrigins('a.domain.com', 'b.domain.com')
This CORS middleware feature can be used to specify/override which hosts are allowed to send requests to the server (by endpoint). This automatically handles setting the appopriate Access-Control-Allow-Origin
HTTP header.
const express = require('express')
const app = express()
const API = require('@butlerlogic/common-api')
API.applySimpleCORS(app)
app.get('/special/endpoint, API.allowOrigins('a.domain.com', 'b.domain.com'), (req, res) => {...})
This can also be applied to all requests:
app.use(API.allowOrigins('a.domain.com', 'b.domain.com'))
allowPreflight()
This middleware responds to OPTIONS
requests with a 200 OK
response. This method is useful because it automatically applies the appropriate CORS configurations to support any request headers submitted to the endpoint.
app.use(API.allowPreflight())
// or
app.any('/path', API.allowPreflight(), (req, res) => { ... })
allowAll(host)
This middleware uses CORS, allowing any request from the specified host(s). This should not be considered a secure or insecure method. Used appropriately, it can provide proper security at large scale. Used inappropriately, it can be insecure at any scale. Use with caution. Remember, this method is primarily useful for developing functional API's before locking them down with tighter security restrictions.
// Allow anything from any domain (insecure)
app.use(API.allowAll('*'))
app.use(API.allowAll()) // Equivalent of above
// Allow anything from my domain (semi-secure, limited to 1 domain)
app.use(API.allowAll('mydomain.com'))
// Applied to a specific endpoint (semi-secure, limited to 1 path on 1 domain)
app.get('/endpoint', this.allowAll('mydomain.com'), (req, res) => { ... })
// Applied to a specific endpoint for multiple sources
app.get('/endpoint', this.allowAll('mydomain.com', 'mypartner.com'), (req, res) => { ... })
Responses
200
app.post('/endpoint', API.200)
Sends a status code 200
response.
OK
app.post('/endpoint', API.OK)
Sends a status code 200
response.
201
app.post('/endpoint', API.201)
Sends a status code 201
response.
CREATED
app.post('/endpoint', API.CREATED)
Sends a status code 201
response.
401
app.post('/endpoint', API.401)
Sends a status code 401
response.
UNAUTHORIZED
app.post('/endpoint', API.UNAUTHORIZED)
Sends a status code 401
response.
404
app.post('/endpoint', API.404)
Sends a status code 404
response.
NOT_FOUND
app.post('/endpoint', API.NOT_FOUND)
Sends a status code 404
response.
501
app.post('/endpoint', API.501)
Sends a status code 501
response.
NOT_IMPLEMENTED
app.post('/endpoint', API.NOT_IMPLEMENTED)
Sends a status code 501
response.
OTHER_STATUS_CODES
All of the standard status codes have shortcut methods available. Each HTTP status code has two methods associated with it: HTTP###
and a method named by replacing spaces and hyphens in the the status message with underscores, removing special characters, and converting the whole message to upper case.
For example, HTTP status 304 (Not Modified) would have a method HTTP304
and NOT_MODIFIED
.
The joke status, 418 (I'm a Teapot) illustrates how special characters are removed.
HTTP418()
IM_A_TEAPOT()
redirect(url, [permanent, moved])
A helper method for redirecting requests to another location. This is not a proxy, it does not actually forward requests. It tells the client the URL being requested is outdated and/or has been moved permanently/temporarily.
This method exists since redirects have been used incorrectly in the past (by the entire industry). The redirect HTTP status codes changed in RFC 7231 (2014), but are still commonly disregarded. This method supplies the appropriate HTTP status codes without having to remember which code is proper for each circumstance.
app.get('/path', API.redirect('https://elsewhere.com')) // 307
// ^ equivalent of: app.get('/path', API.redirect('https://elsewhere.com', false, false))
app.get('/path', API.redirect('https://elsewhere.com', true, false)) // 308
app.get('/path', API.redirect('https://elsewhere.com', true, true)) // 301
app.get('/path', API.redirect('https://elsewhere.com', false, true)) // 303
| Code | Purpose | Description | | - | - |- | |301|Moved Permanently|This and all future requests should be directed to the given URI.| |303|See Other|The response to the request can be found under another URI using the GET method. When received in response to a POST (or PUT/DELETE), the client should presume that the server has received the data and should issue a new GET request to the given URI.| |307|Temporary Redirect|In this case, the request should be repeated with another URI; however, future requests should still use the original URI. In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request. For example, a POST request should be repeated using another POST request.| |308|Permanent Redirect|The request and all future requests should be repeated using another URI. 307 and 308 parallel the behaviors of 302 and 301, but do not allow the HTTP method to change. So, for example, submitting a form to a permanently redirected resource may continue smoothly.|
302 (Found) is not a valid redirect code.
Tells the client to look at (browse to) another URL. 302 has been superseded by 303 and 307. This is an example of industry practice contradicting the standard. The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect (the original describing phrase was "Moved Temporarily"),[21] but popular browsers implemented 302 with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307 to distinguish between the two behaviours.[22] However, some Web applications and frameworks use the 302 status code as if it were the 303.
/**
* Redirect the request to another location.
* @param {string} url
* The location to redirect to.
* @param {boolean} [permanent=false]
* Instruct the client that the redirect is permanent,
* suggesting all future requests should be made directly
* to the new location. When this is `false`, a HTTP `307`
* code is returned. When `true`, a HTTP `308` is returned.
* @param {boolean} [moved=false]
* Inform the client that the destination has been moved.
* When _moved_ is `true` and _permanent_ is `false`, an
* HTTP `303` (Found) status is returned, informing the
* client the request has been received and a `GET` request
* should be issued to the new location to retrieve it. When
* _permanent_ is `true`, a HTTP `301` is returned,
* indicating all future requests should be made directly to
* the new location.
*/
reply(anything)
A helper method to send objects as a JSON response, or to send plain text. This function attempts to automatically determine the appropriate response header type.
Example:
app.get('/path', API.reply(myJsonObject))
replyWithError(res, [status, message]|error)
Send an HTTP error response. This function accepts two different kinds of arguments. The response is always the first argument. The method will also accept a custom HTTP status code and/or a custom plaintext message, as shown here:
app.get('/myendpoint', (req, res) => {
if (problem === true) {
API.replyWithError(res, 400, 'There is a problem.')
}
})
By default, an HTTP status code of 500
(Server Error) is used.
Another option it to pass a JavaScript error as the last argument.
app.get('/myendpoint', (req, res) => {
someFunction((err, data) => {
API.replyWithError(res, err)
})
})
// A custom HTTP status code can be used
app.get('/myendpoint', (req, res) => {
someFunction((err, data) => {
API.replyWithError(res, 404, err)
})
})
In the first example, an error is passed as the last argument. Using this approach, the response will have a 400
status and the message will be auto-extracted from the JavaScript error. The second example will do the same thing, but it will send a 404
status code instead of the default.
replyWithMaskedError(res, [status, message]|error)
This functions very similarly to replyWithError
, but a non-descript error message is sent to the client with a reference ID. The message/error is written to the console, making it possible to lookup actual error in the server logs.
For example:
app.get('/myendpoint', (req, res) => {
if (problem === true) {
API.replyWithMaskedError(res, 400, 'There is a problem connecting to the database.')
}
})
The response sent in the reply will actually look like:
400 An error occurred. Reference: eaac53bc-8b95-4e81-bc29-dead2a14c2ea
The logs would look like:
[ERROR:eaac53bc-8b95-4e81-bc29-dead2a14c2ea] (400) There was a problem connecting to the database.
Utilities
createUUID
This utility method helps generate unique ID's. This is used to generate the transaction ID for masked error output (replyWithMaskedError
method).
atob(value)
ASCII to Binary: This mimics the window.atob function. It is commonly used to extract username/password from a request.
applyBaseUrl (req, route = '/', forceTLS = false)
Apply the base URL to the specified route. If forceTLS
is set to true
, the response will always use the https
protocol.
app.get('/my/path', (req, res) => {
res.json({
id: API.applyBaseURL(req, 'myid')
})
})
If a request was made to http://domain.com/my/path
, this would return:
{
"id": "http://domain.com/myid"
}
applyRelativeUrl (req, route = '/', forceTLS = false)
Apply the relative URL to the specified route. If forceTLS
is set to true
, the response will always use the https
protocol.
app.get('/my/path', (req, res) => {
res.json({
id: API.applyBaseURL(req, 'myid')
})
})
If a request was made to http://domain.com/my/path
, this would return:
{
"id": "http://domain.com/my/path/myid"
}
errorType
By default, using replyWithError or replyWithMaskedError will produce standard text-based error reponses, such as 401 (Unauthorized)
.
It is possible to produce JSON instead, resulting in:
{
"status": 401,
"message": "Unauthorized"
}
If JSON is needed, set the errorType to json
.
API.errorType = 'json'
commonHeaders
This is an array of the most common request headers used by HTTP clients. This is useful when constructing your own list of CORS headers using the allowHeaders
method.
console.log(API.commonHeaders)
Headers include: Origin
, X-Requested-With
, Content-Type
, and Accept
. This list may be updated from time to time.
httpMethods
An array of the official HTTP methods.
console.log(API.httpMethods)
Includes:
GET
HEAD
POST
PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH