steady-api
v2.1.0
Published
Configurable REST API built with Express and TypeScript
Downloads
4
Maintainers
Readme
Steady
A simple, configurable, Express based API framework to help you create new JSON based REST APIs very quickly and easily.
Features include:
- Auto-generated documentation
- Easily customisable with Express Middlewares
- Consistent approach to building APIs
- Parameter validation via Joi
- Customisable parameter types
- Authentication per route
- Basic Typescript support
- Allows custom static content (HTML, etc...)
- Attach other HTTP components to the Steady server (such as Socket.IO)
Basic usage
To install, do npm install steady-api
Then to get started with Steady, you will need to do three things:
- Create a controller and some actions
- Define a route
- Create a new API with this controller and route
Create controller
In the root of your project:
mkdir controllers
touch controllers/user.js
In user.js
we can create our first action:
// connect to some kind of database
const db = new DB();
// fetch function for retrieving a user
const fetch = (params, callback) => {
// attempt to retrieve the user from the DB
db.getUser(params.userId, (err, user) => {
// if there was an error (user not found)
// then return with an error
if (err) return callback({
status: 404,
errorMessage: 'This user was not found',
errors: [
`thingId ${params.userId} not found`
]
});
// optionally remove the users meta data
if (!params.include_meta) {
user.meta = null;
}
// return the user
return callback(null, user);
});
}
module.exports = {
fetch: fetch
}
Define a route
In the root of your project:
mkdir routes
touch routes/user.json
In user.json
we describe our first route:
[
{
"description": "Retrieve a user",
"method": "GET",
"url": "/user/:userId",
"controller": "user",
"action": "fetch",
"authentication": {
"controller": "auth",
"action": "simple"
},
"params": [
{
"name": "include_meta",
"required": false,
"type": "boolean",
"default": false
}
]
}
]
This route definition does the following:
- Defines an active route for
GET /user/:userId
, whereuserId
can be captured as a parameter to be used later (see Express documentation on routing for more info) - Defines the controller to be used (
user.js
) - Defines the action to be user from within the controller (
fetch
) - Defines whether or not authentication is required for this route (more info)
- Defines an additional parameter
include_meta
that can be used in this request
All properties except for params
and authentication
are required when defining a route
You can create multiple definitions within each route definition file, and you can have multiple definition files within the specified routesDir
(see below).
Authentication
Authentication can be handled on a per route basis, and configuration is very similar to defining a controller/action for your route:
{
...
"authentication": {
"controller": "auth",
"action": "simple",
}
...
}
This example shows that for this particular route, we want to use the auth.js
controller and the simple
method from within this controller.
All validation in Steady is created using Express Middleware. This is what our simple authentication might look like:
// auth.js
const simple = (req, res, next) => {
const validCredentials = (req.query.username === 'valid_username' && req.query.password === 'valid_password');
if (validCredentials) return next();
return res.status(401).send("Unauthorized");
}
module.exports = {
simple
}
If the request includes valid credentials, we call next()
and carry on processing the request. If not, we return an error.
Because we are just using Express Middleware, it is very simple to use existing authentication middleware such as Passport.
Create application
From the root of your project:
touch app.js
In app.js
we will create our new Steady API:
const Steady = require('steady-api').Steady;
const app = new Steady({
controllersDir: './controllers',
routesDir: './routes',
apiName: 'Example API'
});
Here we define the directories that our Controllers and Route Definitions live in. We can also give our API a name. And that's it! Run the API by doing:
node app.js
Your API will now be running on port 5000
and you can make requests like so:
curl 'http://localhost:5000/user/123'
curl 'http://localhost:5000/user/123?include_meta=true'
Request & Response
One of the main drivers behind creating Steady was the desire for consistent responses, and as a result Steady is very opinionated on how it delivers these responses.
Each controller action must be defined in the following way:
/**
* params - contains the values passed in as parameters
* callback - callback function expecting two arguments:
* > err - erroroneous response data
* > data - successful response data
*/
const action = function(params, callback) {
// do something...
return callback(err, data);
}
Erroneous Response
If you wish to send an error response you must provide an object with the following parameters as the err
value in the callback:
status
- the HTTP status number (e.g.400
for a Bad Request status - more info)errorMessage
- A human readable error message (e.g. 'Not all required fields were provided')errors
- An array of specific error messages (e.g.['name is a required parameter', 'age must be a number']
)
The response returned to the user will look something like this:
// HTTP/1.1 400 Bad Request
{
"errorMessage": "This request failed validation, please check the documentation for GET /example",
"errors": [
"'name' with value 'David' fails to match the required pattern: /^A.{3,4}$/"
],
"request": {
"body": {
// data passed in the body of the request
},
"method": "POST",
"query": {
// data passed in the query string of the request
},
"url": "/thing"
}
}
Successful Response
A successful response is much simpler, simply pass anything into the data
argument of the callback whilst also passing null
into the err
argument of the callback. The response looks something like this:
NOTE: the response format has changed since 1.x.x
// HTTP/1.1 200 OK
{
// whatever gets passed into the 'data' argument
}
Currently, all successful requests are returned with a HTTP 200
response.
More Options
There are numerous features and options available to customise and tailor your Steady experience...
Configuration
When creating a new instance of Steady, some configuration must be supplied in order to get up and running - however a number of sensible defaults are set in for the majority of the configuration.
A new instance of Steady is created like this: const app = new Steady( options );
Configuration is passed in via the options
object, which has the following properties:
controllersDir
required - path to a directory that contains your controllers (e.g../controllers
)routesDir
required - path to a directory that contains your route definition files (e.g../routes
)port
default:5000
- the port your server will be listening onapiName
default:API
- the name of this API. Will be referenced in the docs, etc...docsPath
default:/
- the path where the API docs will be availableapiPath
default:/
- the path where the API routes will be served fromcustomTypes
- define custom types for use in your Route definitions, find out more in the Custom Types section of this documentmiddleware
- define the Express Middleware you wish to use, find out more in the Middleware section of this document
Route Parameters
When defining Routes in Steady, you can describe the different parameters you wish to use. Part of this process is defining what type this parameter is. This helps Steady to validate that your users are passing in the correct data.
All parameter definitions can include the following information:
{
"name": "foo", // required - name of the parameter
"required": true, // required - is this parameter required, true/false
"type": "string", // required - the type of the parameter
"default": "bar", // this will be the default value of this parameter if it is not provided by the user
"example": "Example String" // An example of what you are expecting from the user, to be shown in the docs (sensible defaults applied if not provided)
}
Steady provides 8 different types out of the box:
string
number
boolean
enum
date
url
email
file
note: if you wish to use the file
parameter type you must submit your data with multipart/form-data
encoding.
Some of these types also come with further configuration options:
string
A simple string, any amount of any characters.
You can also define a regex
property to enforce a particular pattern
{
"name": "foo",
"required": false,
"type": "string",
"regex": "^[A-Z].+" // string must start with an upper case letter
}
number
A numeric value, integers or floats.
You can also define a min
and/or max
property to define a range
{
"name": "foo",
"required": false,
"type": "number",
"min": 10, // minimum value of 10
"max": 100 // maximum value of 100
}
enum
A defined set of values.
You must define a values
property to describe the allowed options.
{
"name": "foo",
"required": false,
"type": "enum",
"values": ["bar", "baz", "xyz"]
}
Custom Types
Although Steady has provided you with 7 standard types out of the box, it is very possible that they won't be enough to cover the unique use cases of your applications. So to help you with this, Steady allows you to create custom types to fit these use cases.
Validation in Steady is handled by Joi, and you can also use Joi to define your own parameter types along with the necessary validation to go with it.
Here is an example of creating a new point
type, which requires an array with 2 numeric elements.
const Steady = require('steady-api').Steady;
const Joi = require('joi');
const app = new Steady({
controllersDir: './controllers',
routesDir: './routes',
customTypes: [
{
"name": "point", // the name of our new type
"validation": Joi.array().length(2).items(Joi.number().required(), Joi.number().required()) // Joi validation for this type
}
]
});
Once your new types are defined, you can use them when defining your Route parameters, just like any other type:
{
"name": "foo",
"type": "point",
"default": [1,1],
"required": true
}
Middleware
As mentioned earlier, Steady is based on Express, and as such allows you to use all of your favourite Express Middleware!
For example, lets say you want to add the Compression middleware you could just do:
const Steady = require('steady-api').Steady;
const compression = require('compression');
const app = new Steady({
controllersDir: './controllers',
routesDir: './routes',
apiName: 'Example API',
middleware: [
compression()
]
});
Additional routes
Again, Steady is just an Express app at heart, so if you wish to define additional routes outside of your API, you can do so!
const Steady = require('steady-api').Steady;
const app = new Steady({
controllersDir: './controllers',
routesDir: './routes',
apiName: 'Example API'
});
app.get('/example', (req, res) => {
res.send('This is an example');
})
You can also use app.post
, app.put
, app.delete
or app.all
- just like in Express.
Documentation
One of the advantages of using Steady is that documentation is auto-generated for you!
By default, the docs will be available at /
but can be changed by specifying a docsPath
when configuring your Steady API.
Static Content
Steady allows you to serve out static content, simply provide the staticContentDir
option when starting your app, pointing Steady to the location of your static content. All content is served out at /
.
const Steady = require('steady-api').Steady;
const app = new Steady({
controllersDir: './controllers',
routesDir: './routes',
apiName: 'Example API',
staticContentDir: './content'
});
Attach other HTTP Components
If you want to attach other HTTP Components (such as Socket.IO) to your Steady app, you can do this by defining the httpAttach
object as shown here:
const Steady = require('steady-api').Steady;
const io = require('socket.io')
const app = new Steady({
controllersDir: './controllers',
routesDir: './routes',
apiName: 'Example API',
httpAttach: {
io: io
}
});
note: this experimental and only tested with Socket.IO. As long as the component you wish to use attaches to the node http
server in the same way as Socket.IO, it should be fine
Typescript
Steady also has some basic Typescript support.
Firstly, do npm install --save-dev @types/express @types/joi
From now onwards it's mostly the same as described above, but here's a quickstart guide:
Creating your app
import { ISteadyOptions, Steady } from 'steady-api';
Import * as Joi from 'joi';
// Make sure your options conforms to the required format
// including the validation for your custom types
const opts: ISteadyOptions = {
controllersDir: './build/controllers',
routesDir: './routes',
customTypes: [
{
"name": "point",
"validation": Joi.array().length(2).items(Joi.number().required(), Joi.number().required())
}
],
}
const app = new Steady(opts);
Controllers
import { IErrorData, ISuccessData } from 'steady-api';
// enforce types for the arguments in the callback function
const getThing = (params, callback: (err: IErrorData, data?: ISuccessData) => void) => {
const thing = getThing(params.thingId)
if (!thing) {
return callback({
status: 404,
errorMessage: 'This thing was not found',
errors: [
`thingId ${params.thingId} not found`
]
})
}
return callback(null, thing);
}