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-apiLet'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-Totalwill contain the total count of results. The query parametersoffset,limitandsortcan be used to sort by a field, specify the range of users to return. By defaultoffset = 0andlimit = 100. The query parameterwherecan be used to filter results.GET /users/:id: returns the single user having theidornullif not found.GET /users/:id/posts: returns the list of posts of a specific user. The query parametersoffset,limitandsortare supported.POST /users: adds and returns a new user with the data inreq.body. Giving thepostsattribute 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. Thepostsattribute 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 theidand sets thewriterattribute of his posts tonull. -
GET /posts: similar toGET /users, the format of a post is{id, category, writer, title, content}.categoryandwritercontains 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 attributeswriterandcategorycan 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 thepostsof 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 ornullif missing. -
GET /categories/:id/parent: returns the parent of the category ornullif missing. -
GET /categories/:id/children: returns the list of subcategories of the category ornullif missing. -
GET /categories/:id/posts: returns the list of posts of the category. -
POST /categories: adds and returns a new category. The attributesparent,childrenandpostscan 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 tonulland removes it from the children of its parent if any.
Now you may say:
Okay, that's cool. But the
passowrdof 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,putanddelete. -
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) => RelationCreates 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) => RelationCreates 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) => RelationCreates 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) => RelationCreates 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) => RouteCreate 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} = {}) => RouteCreates a POST route to /login (overwritten by uri if given) that performs an authentication as follows:
-
Checks that
req.bodyhas the specifiedfields. if a field is missing then aWrongCredentialsErrorerror is thrown. -
Checks that a record of the given
modelwith fields values is present on database. if not aWrongCredentialsErroris thrown. -
returns a response of format
{token: '....'}containing a JSON Web Token to be used for authentication.
(Model model, {converter, uri, actions}) => RouteConstructs 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 allObjectandArrayfields. - The offset parameter is set from query parameter
offset, same forlimit,sort, andwhereparameters. - The
whereparameter is parsed as JSON and used as query conditions if given. - Default values for offset and limit are
0and100respectively. No sort is defined by default. - The response header
Content-Totalwill contain the total count of items matching thewhereconditions.
(Model model, {converter, uri, actions}) => RouteConstructs 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}) => RouteConstructs 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
relationsby synchronizing the corresponding documents from other models if needed.
(Model model, {converter, uri, actions, relations}) => RouteConstructs 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
relationsby synchronizing the corresponding documents from other models if needed.
(Model model, {converter, uri, actions, relations}) => RouteConstructs 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
relationsby synchronizing the corresponding documents from other models if needed.
(Relation relation, {uri, converter, actions}) => RouteConstructs 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-oneormany-onerelation then the route is similar toshowwith a uri/sources/:id/target. - if
one-manyormany-manyrelation then the route is similar tolistwith 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) => RouteActionCreates 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) => MiddlewareCreates 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) => MiddlewareTakes 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 | nullReads the query from the request object or returns null if no query is set.
(Model model) => MiddlewareTakes 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) => ObjectReads the data from the request object or returns null if no data is set.
((Request => Promise(Object)) dataGetter) => MiddlewareSimilar to setQuery. The resulting middleware will provide the request to dataGetter and set resulting data.
((Request => Promise(Any)) converterGetter) => MiddlewareThe 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) => StringGets 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) => StringGets the running route model name from the request.
(Request req) => StringGets 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) => MiddlewareReturns a middleware that will set the offset value from the query parameter with name queryParamName or use the defaultValue if missing.
(Request req) => NumberReads the offset from the request object.
Similar to setOffset and getOffset.
(Array(Route) routes) => Routermakes an express router from an array of routes.
(Array(Model) models, Array(Relation) relations, {_all, Model1, Model2, ...}) => RouterReturns 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 fielditemsLengthis auto-updated if present. -
1.5.0: The default resource converter now returns fields of type
ObjectId. -
1.4.0: The response of
listandshowRelatedcontains now a headerContent-Totalequal to the total count of items; useful for pagination. -
1.3.0: The query parameter
whereis now used to filter results onlistandshow-many-relatedroutes. -
1.2.0:
req.bodyis now used to filter results onlistandshow-many-relatedroutes. -
1.0.0: A complete new version is finally out!