wajez/api


REST API Development made easy.

  • Users starred: 7
  • Users forked: 2
  • Users watching: 7
  • Updated at: 2018-09-16 21:50:36

Wajez API

Build Status Join the chat at https://gitter.im/wajez/api Coverage Status Donate Software License

REST API Development made easy.

Contents

What is that?

Wajez API is a library built on top of express and mongoose to make developing REST APIs as easy as possible while being able to control everything and override any behavior. I tried to build this library following the Functional Programming style using the beautiful library Sanctuary.

Installation

npm i --save wajez-api
# or
yarn add wajez-api

Basic Usage

Let's create a REST API for a simple blog, we will define our models using mongoose:

demo/models/User.js

const mongoose = require('mongoose')
const {Schema} = mongoose

const User = mongoose.model('User', new Schema({
  posts: [{
    type: Schema.Types.ObjectId,
    ref: 'Post'
  }],
  name: String,
  type: {
    type: String,
    enum: ['author', 'admin']
  },
  email: {
  	type: String,
  	match: /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
  },
  password: {
  	type: String,
  	minLength: 8
  }
}))

demo/models/Post.js

const Post = mongoose.model('Post', new Schema({
  category: {
    type: Schema.Types.ObjectId,
    ref: 'Category'
  },
  writer: {
    type: Schema.Types.ObjectId,
    ref: 'User'
  },
  title: String,
  content: String
}))

demo/models/Category.js

const Category = mongoose.model('Category', new Schema({
  parent: {
    type: Schema.Types.ObjectId,
    ref: 'Category'
  },
  children: [{
    type: Schema.Types.ObjectId,
    ref: 'Category'
  }],
  posts: [{
    type: Schema.Types.ObjectId,
    ref: 'Post'
  }],
  name: String
}))

Now we can write the application using express and wajez-api

const express = require('express')
const bodyParser = require('body-parser')
const {api, oneMany} = require('wajez-api')
const {User, Category, Post} = require('./models')

// create the app
const app = express()
// body parser is required since the routes generated by wajez-api
// assume that req.body already contains the parsed json
app.use(bodyParser.urlencoded({limit: '50mb', extended: true}))
app.use(bodyParser.json({limit: '50mb'}))

// define the models and relations
const models = [User, Category, Post]
const relations = [
  oneMany('User', 'posts', 'Post', 'writer'),
  oneMany('Category', 'posts', 'Post', 'category'),
  oneMany('Category', 'children', 'Category', 'parent')
]

// add routes for resources User, Post and Category
app.use(api(models, relations))

// add an error handler
app.use((err, req, res, next) => {
  console.error(err)
  res.status(500).json({error: err})
})

That's all, we now have a functional REST API with the following routes and features:

  • GET /users: returns an array of users with format {id, name, type, email, password}. The response header Content-Total will contain the total count of results. The query parameters offset, limit and sort can be used to sort by a field, specify the range of users to return. By default offset = 0 and limit = 100. The query parameter where can be used to filter results.
  • GET /users/:id: returns the single user having the id or null if not found.
  • GET /users/:id/posts: returns the list of posts of a specific user. The query parameters offset, limit and sort are supported.
  • POST /users: adds and returns a new user with the data in req.body. Giving the posts attribute as array of ids will update the corresponding posts to use the added user as their writer. if some of the posts are missing or have already a writer, an error is returned.
  • PUT /users/:id: modifies the user having the id. The posts attribute can be given as array of ids or as an object of format:
{
  add: [...], // array of post ids to add
  remove: [...], // array of post ids to remove
}

The writer attribute of the involved posts will be updated accordingly.

  • DELETE /users/:id: removes the user having the id and sets the writer attribute of his posts to null.

  • GET /posts: similar to GET /users, the format of a post is {id, category, writer, title, content}. category and writer contains the identifiers of the related resources.

  • GET /posts/:id: similar to GET /users/:id.

  • GET /posts/:id/writer: returns the writer (user) of a specific post.

  • GET /posts/:id/category: returns the category of a specific post.

  • POST /posts: add and returns a new post. the attributes writer and category can be given then the associations will be updated.

  • PUT /posts/:id: modifies a specific post.

  • DELETE /posts/:id: removes a specific post and removes its id from the posts of the corresponding user and category.

  • GET /categories: similar to GET /users. The format of a category is {id, parent, name}

  • GET /categories/:id: returns a specific category or null if missing.

  • GET /categories/:id/parent: returns the parent of the category or null if missing.

  • GET /categories/:id/children: returns the list of subcategories of the category or null if missing.

  • GET /categories/:id/posts: returns the list of posts of the category.

  • POST /categories: adds and returns a new category. The attributes parent, children and posts can be given then the associations will be updated.

  • PUT /categories/:id: modifies a specific category.

  • DELETE /categories/:id: removes a category, sets the parent of its children to null, sets the category of its posts to null and removes it from the children of its parent if any.

Now you may say:

Okay, that's cool. But the passowrd of the user should not be part of the response. How do I hide it? What if I want run a custom query or transform the data before sending it? is that possible?

Yes, all that is possible and easy to do. Check this demo application for a complet example.

API Reference

Let's start by listing the defined data types in this library.

Types

Express Types

  • Router: an express Router.

  • Request: an express Request.

  • Response: an express Response.

  • Middleware: an express Middleware which is a function like (req, res, next) => {...}.

  • ErrorHandler: an express ErrorHandler which is a function like (err, req, res, next) => {...}.

Route Types

  • RouteMethod: one of get, post, put and delete.

  • RouteAction: an object of format

{
  step: Number,
  middlewares: Array(Middelware)
}
  • Route: an object of format
{
  uri: String,
  method: RouteMethod,
  actions: Array(RouteAction)
}

Query Types

  • Query: one of CreateQuery, FindQuery, CountQuery, UpdateQuery, and RemoveQuery.

  • CreateQuery: an object of format

{
  type: 'create',
  data: Object,
  relations: Array(Relation)
}
  • FindQuery: an object of format
{
  type: 'find',
  conditions: Object,
  projection: String | null,
  options: Object,
  populate: Array({
    path: String,
    match: Object,
    select: String | null,
    options: Object
  })
}
  • CountQuery: an object of format
{
  type: 'count',
  conditions: Object
}
  • UpdateQuery: an object of format
{
  type: 'update',
  conditions: Object,
  data: Object,
  relations: Array(Relation)
}
  • RemoveQuery: an object of format
{
  type: 'remove',
  conditions: Object,
  relations: Array(Relation)
}

Others

  • Relation: represents the relation between two models. in has the following format
{
  type: 'one-one' | 'one-many' | 'many-one' | 'many-many',
  source: {
    name: String, // the source model name
    field: String | null // the source model field, if any.
  },
  target: {
    name: String, // the target model name
    field: String | null // the target model field, if any.
  }
}

Relations

oneOne

(String sourceModelName, String sourceModelField, String targetModelName, String targetModelField) => Relation

Creates a One to One relation between two models.

Example

const User = mongoose.model('User', new mongoose.Schema({
  account: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Account'
  },
  ...
}))

const Account = mongoose.model('Account', new mongoose.Schema({
  owner: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  },
  ...
}))

const relation = oneOne('User', 'account', 'Account', 'owner')

Note: The target field name can be null if the field is absent.

oneMany

(String sourceModelName, String sourceModelField, String targetModelName, String targetModelField) => Relation

Creates a One to Many relation between two models.

Example

const User = mongoose.model('User', new mongoose.Schema({
  posts: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Post'
  }],
  ...
}))

const Post = mongoose.model('Post', new mongoose.Schema({
  writer: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  },
  ...
}))

const relation = oneMany('User', 'posts', 'Post', 'writer')

Note: The target or the source field name can be null if the field is absent. But not both!

manyOne

(String sourceModelName, String sourceModelField, String targetModelName, String targetModelField) => Relation

Creates a Many to One relation between two models.

oneMany(A, a, B, b) === manyOne(B, b, A, a)

manyMany

(String sourceModelName, String sourceModelField, String targetModelName, String targetModelField) => Relation

Creates a Many to Many relation between two models.

Example

const Post = mongoose.model('Post', new mongoose.Schema({
  tags: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Tag'
  }],
  ...
}))

const Tag = mongoose.model('Tag', new mongoose.Schema({
  posts: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Post'
  }],
  ...
}))

const relation = manyMany('Tag', 'posts', 'Post', 'tags')

Note: The target or the source field name can be null if the field is absent. But not both!

Routes

get, post, put, remove

(String uri, Array(RouteAction) actions) => Route

Create a GET, POST, PUT and DELETE route respectively, with the given uri and actions.

Example

const {router, action, get} = require('wajez-api')
const app = express()

// a hello world route
const hello = get('/hello', [
  action(1, (req, res, next) => res.send('Hello World!'))
])

app.use(router([hello]))
// GET /hello will print "Hello World!"

extend

(Route r, {method, uri, actions}) => Route

Extends the route r by overriding its method or uri if given, and adding actions if given.

Example

const {router, action, get, extend} = require('wajez-api')
const app = express()

const hello = get('/hello', [
  action(1, (req, res, next) => res.send('Hello World!'))
])

const yo = extend(hello, {
  actions: [
    action(0, (req, res, next) => res.send('Yo!'))
  ]
})

console.log(yo)
// {
//   method: 'get',
//   uri: '/hello',
//   actions: [
//     action(1, [
//       (req, res, next) => res.send('Hello World!')
//     ]),
//     action(0, [
//       (req, res, next) => res.send('Yo!')
//     ])
//   ]
// }

app.use(router([yo]))
// GET /hello will print "Yo!"

Note: actions of a route are sorted by their steps, that's why in the previous example, even if the yo route contains two actions, the one with step 0 is executed first.

login

(String secret, Model model, Array(String) fields, {uri, actions} = {}) => Route

Creates a POST route to /login (overwritten by uri if given) that performs an authentication as follows:

  • Checks that req.body has the specified fields. if a field is missing then a WrongCredentialsError error is thrown.

  • Checks that a record of the given model with fields values is present on database. if not a WrongCredentialsError is thrown.

  • returns a response of format {token: '....'} containing a JSON Web Token to be used for authentication.

list

(Model model, {converter, uri, actions}) => Route

Constructs a route that returns a list of the given model, then merges the converter if given with the default converter, and extends the route using the uri and actions if any. By default:

  • GET /plural-of-model-name
  • The default converter returns only fields of types (ObjectId, String, Number, Boolean, Buffer, Date). It ignores all Object and Array fields.
  • The offset parameter is set from query parameter offset, same for limit, sort, and where parameters.
  • The where parameter is parsed as JSON and used as query conditions if given.
  • Default values for offset and limit are 0 and 100 respectively. No sort is defined by default.
  • The response header Content-Total will contain the total count of items matching the where conditions.

show

(Model model, {converter, uri, actions}) => Route

Constructs a route that returns a specific document of the given model by id, then merges the converter if given with the default converter, and extends the route using the uri and actions if any. By default:

  • GET /plural-of-model-name/:id.
  • The default converter is the same as list.

add

(Model model, {converter, uri, actions, relations}) => Route

Constructs a route that adds new document of the given model, then merges the converter if given with the default converter, and extends the route using the uri and actions if any. By default:

  • POST /plural-of-model-name.
  • The default converter is the same as list.
  • Handles relations by synchronizing the corresponding documents from other models if needed.

edit

(Model model, {converter, uri, actions, relations}) => Route

Constructs a route that modifies a document of the given model, then merges the converter if given with the default converter, and extends the route using the uri and actions if any. By default:

  • PUT /plural-of-model-name/:id.
  • The default converter is the same as list.
  • Handles relations by synchronizing the corresponding documents from other models if needed.

destroy

(Model model, {converter, uri, actions, relations}) => Route

Constructs a route that removes a document of the given model and extends the route using the uri and actions if any. By default:

  • DELETE /plural-of-model-name.
  • Handles relations by synchronizing the corresponding documents from other models if needed.

showRelated

(Relation relation, {uri, converter, actions}) => Route

Constructs a route that shows related targets for a specific source of the given relation. Then extends it with the given uri, converter and actions. By default:

  • if one-one or many-one relation then the route is similar to show with a uri /sources/:id/target.
  • if one-many or many-many relation then the route is similar to list with a uri /sources/:id/targets.

resource

(Model model, {relations, defaults, list, add, edit, show, destroy, fields}) => Array(Route)

This is a shortcut to generate all resource routes for a model at once, while being able to configure each route. The relations and defaults configurations will be passed to all routes after merging them with the corresponding configuration of the route. The fields specify the configuration of the route showRelated for each field that corresponds to a relation. Check the demo application for examples.

Actions

action

(Number step, Array(Middleware) | Middleware middlewares) => RouteAction

Creates a route action, which is an object that wraps a sequence of middlewares to be executed in a specific step.

Default steps values are

onStart: 1
onReadParams: 2
beforeQuery: 3
onQuery: 4
beforeRun: 5
onRun: 6
beforeConvert: 7
onConvert: 8
beforeSend: 9
onSend: 10
afterSend: 11
inTheEnd: 12

The functions onStart, onReadParams, beforeQuery, onQuery, beforeRun, onRun, beforeConvert, onConvert, beforeSend, onSend, afterSend, and inTheEnd are defined to create actions for the corresponding step; they take a middelware or array of middlewares as argument.

Middlewares

auth

(Object opts, Model model) => Middleware

Creates an authentication middleware. This uses express-jwt internally. The opts are passed to express-jwt then req.user is set to the document of model. Check the demo application for usage example.

setQuery

((Request => Promise(Query)) queryGetter) => Middleware

Takes a function queryGetter as argument and returns a middleware. The function queryGetter should take the request object as parameter and return a promise containing the Query. When the resulting middleware is executed, it will run queryGetter and save the returned query so that we can run it later. See runQuery and getQuery.

getQuery

(Request req) => Query | null

Reads the query from the request object or returns null if no query is set.

runQuery

(Model model) => Middleware

Takes a model and returns a middleware which when executed will run the query (set beforehand) with the given model and set the resulting data in the request object. This data can be read and transformed before being sent in the response. See getData, setData, and convertData.

getData

(Request req) => Object

Reads the data from the request object or returns null if no data is set.

setData

((Request => Promise(Object)) dataGetter) => Middleware

Similar to setQuery. The resulting middleware will provide the request to dataGetter and set resulting data.

convertData

((Request => Promise(Any)) converterGetter) => Middleware

The resulting middleware will call converterGetter with the request, it should return a promise containing a Converter. Then the converter is used to convert the data.

getRoute

(Request req) => String

Gets the running route from the request, the value is one of login, list, add, show, edit, destroy,show-one-related, and show-many-related.

getModel

(Request req) => String

Gets the running route model name from the request.

getRelated

(Request req) => String

Gets the running route related model name from the request. This only makes sense on routes show-one-related and show-many-related; the value is null in other cases.

setOffset

(String queryParamName, Number defaultValue) => Middleware

Returns a middleware that will set the offset value from the query parameter with name queryParamName or use the defaultValue if missing.

getOffset

(Request req) => Number

Reads the offset from the request object.

setLimit, getLimit, setSort, getSort

Similar to setOffset and getOffset.

Routers

router

(Array(Route) routes) => Router

makes an express router from an array of routes.

api

(Array(Model) models, Array(Relation) relations, {_all, Model1, Model2, ...}) => Router

Returns an express router containing all resource routes of all given models. Passes relations and _all config, if given, to all models, merged with the corresponding config if exists.

Development Notes

  • 1.6.0: When updating an array field items, the field itemsLength is auto-updated if present.

  • 1.5.0: The default resource converter now returns fields of type ObjectId.

  • 1.4.0: The response of list and showRelated contains now a header Content-Total equal to the total count of items; useful for pagination.

  • 1.3.0: The query parameter where is now used to filter results on list and show-many-related routes.

  • 1.2.0: req.body is now used to filter results on list and show-many-related routes.

  • 1.1.0: Added extend, login and auth.

  • 1.0.0: A complete new version is finally out!