Yet another RESTful API framework for Express 4.x.
$ npm install exprest4
Note that you have to npm install express
as well because exprest4
doesn't install Express 4.x by itself.
Make controllers
directory and code your Express application.
$ mkdir controllers
$ vim app.js
// app.js
'use strict';
const express = require('express')
, exprest = require('exprest4')
, app = express()
;
exprest.route(app, { url: '/api' })
.then(() => {
app.listen();
});
exprest4
regards each file/directory under the controllers
directory as a controller module in terms of MVC, and routes everything upon route()
call.
Each controller module must have the special property __exprest
as follow:
// controllers/example.js
'use strict';
module.exports = {
__exprest: {
routes: [{
action: 'list'
}, {
action: 'view'
, path: ':id'
}, {
action: 'create'
, method: 'post'
}, {
action: 'update'
, path: ':id'
, method: 'put'
}, {
action: 'remove'
, path: ':id'
, method: 'delete'
}]
}
, list: (req, res) => {
res.json({ action: 'list' });
}
, view: (req, res) => {
res.json({ action: 'view', id: req.params.id });
}
, create: (req, res) => {
res.json({ action: 'create' });
}
, update: (req, res) => {
res.json({ action: 'update', id: req.params.id });
}
, remove: (req, res) => {
res.json({ action: 'remove', id: req.params.id });
}
};
The code implemented in controllers/example.js
is equivalent to the following Express calls:
'use strict';
const example = require('./controllers/example');
app.get('/api/example', example.list);
app.get('/api/example/:id', example.view);
app.post('/api/example', example.create);
app.put('/api/example/:id', example.update);
app.delete('/api/example/:id', example.remove);
If your application works with a database, exprest4
also allows you to define models by using Sequelize.
Make models
directory and add to call model()
as follows:
$ mkdir models
$ vim app.js
exprest4
regards each file/directory under the models
directory as a model module in manner of Sequelize, and loads all models into a Sequelize instance upon model()
call.
// models/project.js
'use strict';
module.exports = (sequelize, DataTypes) => {
return sequelize.define('project', {
title: DataTypes.STRING
, description: DataTypes.TEXT
});
};
// app.js
'use strict';
const express = require('express')
, exprest = require('exprest4')
, app = express()
;
exprest.route(app, { url: '/api' })
.then(() => exprest.model({ dialect: 'sqlite' })) // Use SQLite with memory storage
.then((sequelize) => {
app.locals.models = sequelize.models; // Save the models for controllers
return sequelize.sync();
})
.then(() => {
app.listen();
});
model()
returns Promise
with a Sequelize instance.
It's a good idea to save the models as app.locals.models
as above code in app.js
, so that each controller can access to the models through req.app.locals.models
as the following code:
// controllers/project.js
'use strict';
function project(req)
{
return req.app.locals.models.project;
}
module.exports = {
__exprest: {
routes: [{
action: 'list'
}, {
action: 'view'
, path: ':id'
}, {
action: 'create'
, method: 'post'
}, {
action: 'update'
, path: ':id'
, method: 'put'
}, {
action: 'remove'
, path: ':id'
, method: 'delete'
}]
}
, list: (req, res, next) => {
project(req).findAll()
.then((rows) => { res.json(rows); })
.catch(next);
}
, view: (req, res, next) => {
project(req).findById(req.params.id)
.then((row) => { row ? res.json(row) : res.status(404); })
.catch(next);
}
, create: (req, res, next) => {
project(req).create({
title: req.body.title
, description: req.body.description
})
.then((row) => { res.json(row); })
.catch(next);
}
, update: (req, res, next) => {
project(req).findById(req.params.id)
.then((row) => {
if (!row) {
res.status(404);
return Promise.resolve();
}
return row.update({
title: req.body.title
, description: req.body.description
});
})
.then(() => { res.end(); });
.catch(next);
}
, remove: (req, res, next) => {
project(req).findById(req.params.id)
.then((row) => row ? row.destroy() : Promise.resolve())
.then(() => { res.end(); });
.catch(next);
}
};
Routes controller modules for app
which must be an Express instance.
opts
is an object which may have the following properties:
-
controllers
: String [Default:path.join(process.cwd(), 'controllers')
]
Physical path to where controllers are implemented. -
url
: String [Default:'/'
]
Virtual path prefix for thecontrollers
. -
index
: String [Default:'index'
]
Controller's name which will be mapped directly ontourl
. -
authorizer
: Function [Default:undefined
]
Middleware for user authorization. -
templates
: Object [Default:{ crud: [...] }
]
Templates for routes.
Each controller module implemented in opts.controllers
directory will be mapped onto opts.url
.
You don't have to code for routing by yourself.
Just maintain APIs structure as physical directory structure.
If you call route()
once, which means you have only one controller directory, REST API is going to be flat like /api/<controllers>
.
You can also call route()
more than once to provide structured REST API.
For example, if you want to seperate APIs by version like /api/v1/<controllers>
and /api/v2/<controllers>
, create two controller directories controllers/v1
and controllers/v2
, then call route()
as follow:
// app.js
'use strict';
const path = require('path')
, express = require('express')
, exprest = require('exprest4')
, app = express()
, api_versions = [1, 2]
, controllers_dir = path.join(process.cwd(), 'controllers')
;
Promise.all(
api_versions.map((version) => {
return exprest.route(app, {
url: '/api/v'+version
, controllers: path.join(controllers_dir, 'v'+version)
});
})
)
.then(() => {
app.listen();
});
If your REST APIs require users to be authenticated and/or to be authorized, opts.authorizer
is the right place to implement user authentication/authorization feature.
You can specifiy a middleware as opt.authorizer
to verify a user session, then it will be called at first for every controller.
The following example uses connect-ensure-login as opt.authorizer
:
// app.js
'use strict';
const path = require('path')
, express = require('express')
, exprest = require('exprest4')
, app = express()
, ensureLoggedIn = require('connect-ensure-login').ensureLoggedIn
;
exprest.route(app, {
url: '/api'
, authorizer: ensureLoggedIn('/login')
})
.then(() => {
app.listen();
});
Imports models into a Sequelize instance.
opts
is an object which may have the following properties:
-
models
: String [Default:path.join(process.cwd(), 'models')
]
Physical path to where models are implemented. -
database
: String [Default:undefined
]
Database name. -
username
: String [Default:undefined
]
Username for database authentication. -
password
: String [Default:undefined
]
Password for database authentication.
Each model module implemented in opts.models
directory will be imported into a Sequelize instance after connection to a database is established.
The Sequelize instance will be returned with Promise
upon success.
If a model module has associate()
as a part of classMethods
, it will be called with sequelize.models
as its argument after all models are loaded.
So that it allows you to define associations between models.
opts.database
, opts.username
and opts.password
are passed to Sequelize's constructor.
Other options are also passed through to options
, the 4th arugment of Sequelize's constructor.
Connects to a database through Sequelize by calling Sequelize.authenticate()
.
opts
is an object which may have the following properties:
-
database
: String [Default:undefined
]
Database name. -
username
: String [Default:undefined
]
Username for database authentication. -
password
: String [Default:undefined
]
Password for database authentication.
The Sequelize instance will be returned with Promise
upon success.
Each property of opts
is passed to Sequelize's constructor.
Other options are also passed through to options
, the 4th arugment of Sequelize's constructor.
Synchronizes all defined models to a database.
opts
is an object which may have the following properties:
-
models
: String [Default:path.join(process.cwd(), 'models')
]
Physical path to where models are implemented. -
database
: String [Default:undefined
]
Database name. -
schema
: String [Default:undefined
]
Schema name. -
username
: String [Default:undefined
]
Username for database authentication. -
password
: String [Default:undefined
]
Password for database authentication.
The Sequelize instance will be returned with Promise
upon success.
opts.database
, opts.username
and opts.password
are passed to Sequelize's constructor.
Other options are also passed through to options
of both Sequelize's constructor and Sequelize.sync()
.
Note that this function is equivalent to Sequelize.sync()
unless Sequelize connects to PostgreSQL database.
There is an issue in Sequelize.sync()
for PostgreSQL, even if options.schema
is specified, no schema is prepended to table names while executing dropTable()
.
So that the tables under the public
schema with the same names as your models might be dropped unintentionally by Sequelize.sync()
.
This function provides workaround for the issue.
opts.schema
is passed to each model module implemented in opts.models
directory as the third argument.
The model modules have to pass the schema
as options.schema
to Sequelize.define()
as the following example:
// models/project.js
'use strict';
module.exports = (sequelize, DataTypes, schema) => {
return sequelize.define('project', {
title: DataTypes.STRING
, description: DataTypes.TEXT
}, {
schema: schema
});
};
Each element is an object which has the following properties:
-
action
: String [Required]
Function name to be routed. -
method
: String [Default:'get'
]
Request method to be routed. -
path
: String [Default:''
]
Virtual path for theaction
. -
authorized
: Boolean [Default:true
]
Authorization is required or not for theaction
. -
validator
: Object [Default:undefined
]
Validator(s) for placeholders inpath
. -
middleware
: {Function|Function[]} [Default:undefined
]
Middleware(s) to be passed to Express.
The following properties will be applied to each element in routes
:
-
authorized
: Boolean [Default:true
]
Authorization is required or not. This can be overwritten byroutes[].authorized
. -
validator
: Object [Default:undefined
]
Validator(s) for placeholders in eachpath
. -
middleware
: {Function|Function[]} [Default:undefined
]
Middleware(s) to be passed to Express. -
template
: String [Default:undefined
]
One of templates to be used asroutes
.
If routes[].path
contains any placeholders to receive parameters from req.params
object, validator
will be helpful to validate parameters before calling routes[].action
.
The following example uses validator.js as validator
:
const validator = require('validator');
'use strict';
module.exports = {
__exprest: {
routes: [{
action: 'view'
, path: ':id'
, validator: {
id: validator.isNumeric
}
}]
}
, view: (req, res) => {
// `req.params.id` must a number.
res.json({ id: req.params.id });
}
You can also use a RegExp
object instead of a function as follow:
'use strict';
module.exports = {
__exprest: {
routes: [{
action: 'view'
, path: ':id'
, validator: {
id: /^[1-9][0-9]*/
}
}]
}
, view: (req, res) => {
// `req.params.id` must a number.
res.json({ id: req.params.id });
}
If you have a placeholder commonly used in a controller, you can use preset.validator
as follow:
'use strict';
const validator = require('validator');
module.exports = {
__exprest: {
preset: {
validator: {
id: validator.isNumeric
}
}
, routes: [
action: 'view'
, path: ':id'
}, {
action: 'update'
, path: ':id'
, method: 'put'
}, {
action: 'remove'
, path: ':id'
, method: 'delete'
}]
}
, view: (req, res) => {
res.json({ action: 'view', id: req.params.id });
}
, update: (req, res) => {
res.json({ action: 'update', id: req.params.id });
}
, remove: (req, res) => {
res.json({ action: 'remove', id: req.params.id });
}
};
middleware
is typically used for APIs accept file uploading.
The following example uses Multer as middleware
:
'use strict';
const multer = require('multer')
, upload = multer(/* memory storage */)
;
module.exports = {
__exprest: {
routes: [{
action: 'echo'
, method: 'post'
, middleware: upload.single('message')
}]
}
, echo: (req, res) => {
res.json({ echo: req.file.buffer.toString() });
}
};
Another use case is authentication.
The following example uses Passport as middleware
:
'use strict';
const passport = require('passport');
module.exports = {
__exprest: {
routes: [{
action: 'login'
, middleware: passport.authenticate('basic', { session: false })
}]
}
, login: (req, res) => {
res.json({ loginAs: req.user.username });
}
};
If you want to authenticate users for every action in a controller, you can use preset.middleware
as follow:
'use strict';
const passport = require('passport')
, multer = require('multer')
, upload = multer(/* memory storage */)
;
module.exports = {
__exprest: {
preset: {
middleware: passport.authenticate('basic', { session: false })
}
, routes: [{
action: 'login'
}, {
action: 'echo'
, method: 'post'
, middleware: upload.single('message')
}]
}
, login: (req, res) => {
res.json({ loginAs: req.user.username });
}
, echo: (req, res) => {
res.json({ echo: req.file.buffer.toString() });
}
};
template
is an option to avoid defining the same routes
in multiple controllers.
Once you define routes which might be used often in several controllers, then feed them to route()
with opts.templates
, each controller can refer one of the templates through preset.template
property.
The following example shows how to use template:
// app.js
'use strict';
const express = require('express')
, exprest = require('exprest4')
, app = express()
, templates = {
queue: [{
action: 'push'
, path: ':elem'
, method: 'post'
}, {
action: 'pop'
, method: 'delete'
}]
}
;
exprest.route(app, { templates: templates })
// controllers/seat.js
'use strict';
let waiting_list = [];
module.exports = {
__exprest: {
preset: {
template: 'queue'
}
// No routes required here.
}
, push: (req, res) => {
waiting_list.push(req.params.elem);
res.json(waiting_list);
}
, pop: (req, res) => {
waiting_list.pop();
res.json(waiting_list);
}
};
exprest4
provides the following template crud
by default which might be useful for CRUD-based REST APIs:
[{
action: 'list'
}, {
action: 'view'
, path: ':id'
}, {
action: 'create'
, method: 'post'
}, {
action: 'update'
, path: ':id'
, method: 'put'
}, {
action: 'remove'
, path: ':id'
, method: 'delete'
}]
If you define routes
along with preset.template
, each route in routes
will be appended to the template routes as a result.
In addition, routes
which has exactly the same method
and path
overwrites corresponding routes in the template.
The MIT License (MIT)
Copyright (c) 2016-2018 Hiro Fujita bow.fujita@gmail.com
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.