REST API Development made easy.
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.
npm i --save wajez-api
# or
yarn add wajez-api
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 headerContent-Total
will contain the total count of results. The query parametersoffset
,limit
andsort
can be used to sort by a field, specify the range of users to return. By defaultoffset = 0
andlimit = 100
. The query parameterwhere
can be used to filter results.GET /users/:id
: returns the single user having theid
ornull
if not found.GET /users/:id/posts
: returns the list of posts of a specific user. The query parametersoffset
,limit
andsort
are supported.POST /users
: adds and returns a new user with the data inreq.body
. Giving theposts
attribute as array of ids will update the corresponding posts to use the added user as theirwriter
. if some of the posts are missing or have already awriter
, an error is returned.PUT /users/:id
: modifies the user having theid
. Theposts
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 theid
and sets thewriter
attribute of his posts tonull
. -
GET /posts
: similar toGET /users
, the format of a post is{id, category, writer, title, content}
.category
andwriter
contains the identifiers of the related resources. -
GET /posts/:id
: similar toGET /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 attributeswriter
andcategory
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 theposts
of the corresponding user and category. -
GET /categories
: similar toGET /users
. The format of a category is{id, parent, name}
-
GET /categories/:id
: returns a specific category ornull
if missing. -
GET /categories/:id/parent
: returns the parent of the category ornull
if missing. -
GET /categories/:id/children
: returns the list of subcategories of the category ornull
if missing. -
GET /categories/:id/posts
: returns the list of posts of the category. -
POST /categories
: adds and returns a new category. The attributesparent
,children
andposts
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 tonull
, sets the category of its posts tonull
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.
Let's start by listing the defined data types in this library.
-
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) => {...}
.
-
RouteMethod
: one ofget
,post
,put
anddelete
. -
RouteAction
: an object of format
{
step: Number,
middlewares: Array(Middelware)
}
Route
: an object of format
{
uri: String,
method: RouteMethod,
actions: Array(RouteAction)
}
-
Query
: one ofCreateQuery
,FindQuery
,CountQuery
,UpdateQuery
, andRemoveQuery
. -
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)
}
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.
}
}
(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.
(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!
(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)
(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!
(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!"
(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.
(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 specifiedfields
. if a field is missing then aWrongCredentialsError
error is thrown. -
Checks that a record of the given
model
with fields values is present on database. if not aWrongCredentialsError
is thrown. -
returns a response of format
{token: '....'}
containing a JSON Web Token to be used for authentication.
(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 allObject
andArray
fields. - The offset parameter is set from query parameter
offset
, same forlimit
,sort
, andwhere
parameters. - The
where
parameter is parsed as JSON and used as query conditions if given. - Default values for offset and limit are
0
and100
respectively. No sort is defined by default. - The response header
Content-Total
will contain the total count of items matching thewhere
conditions.
(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
.
(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.
(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.
(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.
(Relation relation, {uri, converter, actions}) => Route
Constructs a route that shows related target
s for a specific source
of the given relation. Then extends it with the given uri
, converter
and actions
. By default:
- if
one-one
ormany-one
relation then the route is similar toshow
with a uri/sources/:id/target
. - if
one-many
ormany-many
relation then the route is similar tolist
with a uri/sources/:id/targets
.
(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.
(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.
(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.
((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.
(Request req) => Query | null
Reads the query from the request object or returns null
if no query is set.
(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.
(Request req) => Object
Reads the data from the request object or returns null
if no data is set.
((Request => Promise(Object)) dataGetter) => Middleware
Similar to setQuery. The resulting middleware will provide the request to dataGetter
and set resulting data.
((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.
(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
.
(Request req) => String
Gets the running route model name from the request.
(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.
(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.
(Request req) => Number
Reads the offset
from the request object.
Similar to setOffset
and getOffset
.
(Array(Route) routes) => Router
makes an express router from an array of routes.
(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.
-
1.6.0: When updating an array field
items
, the fielditemsLength
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
andshowRelated
contains now a headerContent-Total
equal to the total count of items; useful for pagination. -
1.3.0: The query parameter
where
is now used to filter results onlist
andshow-many-related
routes. -
1.2.0:
req.body
is now used to filter results onlist
andshow-many-related
routes. -
1.0.0: A complete new version is finally out!