@ecor/common-api v1.3.0
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.
Installation:
npm install @ecor/common-api -S
Usage
const express = require('express')
const Endpoint = require('@ecor/common-api')
const app = express()
app.post('/endpoint', Endpoint.validateJsonBody, (req, res) => { ... })
const server = app.listen(() => console.log('Server is running.'))
Shortcuts
Request Middleware
- log
- logRequestHeaders
- litmusTest([message])
- validateJsonBody
- validNumericId
- validId
- validResult(res, callback)
- basicauth(user, password)
- bearer(token)
- applyCommonConfiguration(app, [autolog])
- applySimpleCORS(app, host='*')
Responses
- 200
- OK
- 201
- CREATED
- 401
- UNAUTHORIZED
- 404
- NOT_FOUND
- 501
- NOT_IMPLEMENTED
- Other HTTP Status Codes
- 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])
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(Endpoint.log)
app.post('/endpoint', Endpoint.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(Endpoint.logRequestHeaders)
app.post('/endpoint', Endpoint.logRequestHeaders, ...)
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', Endpoint.litmusTest('endpoint reachable'), ...)
validateJsonBody
app.post('/endpoint', Endpoint.validateJsonBody, ...)
Validates a request body exists and consists of valid JSON.
validNumericId
app.post('/endpoint/:id', Endpoint.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', Endpoint.validNumericId('userid'), ...)
validId
app.post('/endpoint/:id', Endpoint.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', Endpoint.validId('userid'), ...)
validResult(res, callback)
Inspects the result and returns a function that will throw an error or return results.
let checkResult = Endpoint.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.
app.get('/secure', Endpoint.basicauth('user', 'passwd'), (req, res) => ...)
It is also possible to perform a more advanced authentication using a custom function. For example:
app.get('/secure', Endpoint.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.
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', Endpoint.bearer('mytoken'), Endpoint.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', Endpoint.bearer(function (token) {
return isValidToken(token)
}), Endpoint.reply('authenticated'))
applyCommonConfiguration(app, autolog)
const express = require('express')
const app = express()
const Endpoint = require('@ecor/common-api')
Endpoint.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:
Endpoint.applyCommonConfiguration(app, false)
applySimpleCORS(app, host='*')
const express = require('express')
const app = express()
const Endpoint = require('@ecor/common-api')
Endpoint.applySimpleCORS(app)
// Endpoint.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, PATCH, DELETE, OPTIONS
Responses
200
app.post('/endpoint', Endpoint.200)
Sends a status code 200
response.
OK
app.post('/endpoint', Endpoint.OK)
Sends a status code 200
response.
201
app.post('/endpoint', Endpoint.201)
Sends a status code 201
response.
CREATED
app.post('/endpoint', Endpoint.CREATED)
Sends a status code 201
response.
401
app.post('/endpoint', Endpoint.401)
Sends a status code 401
response.
UNAUTHORIZED
app.post('/endpoint', Endpoint.UNAUTHORIZED)
Sends a status code 401
response.
404
app.post('/endpoint', Endpoint.404)
Sends a status code 404
response.
NOT_FOUND
app.post('/endpoint', Endpoint.NOT_FOUND)
Sends a status code 404
response.
501
app.post('/endpoint', Endpoint.501)
Sends a status code 501
response.
NOT_IMPLEMENTED
app.post('/endpoint', Endpoint.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 associted 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()
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', Endpoint.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) {
Endpoint.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) => {
Endpoint.replyWithError(res, err)
})
})
// A custom HTTP status code can be used
app.get('/myendpoint', (req, res) => {
someFunction((err, data) => {
Endpoint.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) {
Endpoint.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: Endpoint.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: Endpoint.applyBaseURL(req, 'myid')
})
})
If a request was made to http://domain.com/my/path
, this would return:
{
"id": "http://domain.com/my/path/myid"
}