apis-expressionist
v0.1.0
Published
Expresionist.wrapper(express).for(['Write', 'Document', 'Generate client']).of('REST APIs')
Downloads
4
Readme
node-expressionist
Introduction
Expresionist.wrapper(express).for(['Write', 'Document', 'Generate client']).of('REST APIs')
aka: Wrapper on top of express to Write, Document and 'Create the client'™ of REST APIs
How to write an API ?
First write an YML with your URLs, Parameters, Validations, Constraints, Hooks, Handlers and Documentation. All in one place :)
You could use JSON too, in fact, YML is translated to JSON.
uris.yml
# The identifier must be unique, YML overwrite keys.
get-date-diff: # this will be the client function name camel-cased.
# Type of object, you can define more than just URIs...
type: uri
# array of methods: GET,POST,PUT,DELETE,PATCH,HEAD
methods: [GET]
# entry point
uri: /date-diff
# documentation, it's recommended to use |+ or >
doc: |+
Get the date difference between given and the server.
# FQFN: main function that will manage the request
handler: date.js:diff
# input validation via GET (query)
get:
date:
cast: date
doc: current client date
constraints:
date:
# input validation via POST (body)
post:
# input validation in the uri params
params:
# response validation (if success === true)
response:
diff:
cast: integer
doc: Difference in milliseconds
# custom data, can be retrieve via req.route.data in the handler
data:
#here put your application data, ex: access control using user-permissions
server.js
var Expressionist = require("apis-expressionist"),
expressionist,
express = require("express"),
app = express(),
cookieParser = require('cookie-parser'),
bodyParser = require('body-parser'),
router = express.Router();
// init express
app.use(cookieParser('no-more-secrets'));
app.use(bodyParser.urlencoded({ extended: false }))
// DO NOT: `app.use(router);` will be called for you
// init expressionist
expressionist = new Expresionist(app, router);
expressionist.rootDir = __dirname;
// loading YML file
expressionist.loadYML("uris.yml", function () {
});
// I found interesting to concatenate various files, and group it
// groups can be used to export documentation and sort client code.
expressionist.loadYML(["common-hooks.yml", "users.yml"], "users", function () {
// note: YML exception will not match line in file
expressionist.listen(80);
//save documentation
expressionist.saveDoc();
});
URI definitions (type: uri)
Contains the following parameters
methods (array, required)
GET, POST, PUT, DELETE, PATCH, HEAD
uri (string, required)
If the URL starts with "/", there will be no version-ing (recommended) Otherwise expressionist.version (regexp) will be used to define the final URI.
doc (string, optional)
Documentation text. It's recommended to use "|+" instead of ">" for multi-line text.
params, get, post & files
Input schema. See (utilitario)[https://github.com/llafuente/utilitario] module for more information.
- cast
- constraints, list/object of constraints that the input has to meet.
- object, require "cast: object", define the schema of each key
- items, require "cast: array", define each item schema
- sanitize, not only clean the input also can do some transformation to it, like lowercase
- default, set the default value (that will also be casted!)
requestHooks (array, optional) More info
Callbacks that are executed before the handler.
Note: Must be defined before or will throw.
requestHooksPolicy (string[null, CONTINUE_ON_ERROR], optional)
When any requestHook add an error, the execution stop and return the error to the user.
In some cases you will want to continue even if errors are found like start-transaction / end-transaction
CONTINUE_ON_ERROR will execute all requestHook(DONE) and responseHooks(TODO) but not the handler.
handler (string)
FQFN of the handler.
Note: Parameters count is checked and throw an exception a difference is found.
The handler is a function and must have three parameters (req, res, next)
You can use handlerArguments: EXPAND to add inputs from get/post/param directly as arguments in the function (@todo link to example)
handlerArguments (string[COMPACT,EXPAND], optional) Default: COMPACT
Define how expressionist send arguments to the handler.
- COMPACT: just three arguments, same as express (req, res, next)
- EXPAND: will send input as arguments plus req, res & next.
handlerArgumentsOrder (array, optional) Default: null (will send inputs)
List of names with the arguments wanted. By default use all inputs with the following preference: params, get, post.
Do not handle name collisions. In case GET/POST input has the same name (not recommended!!) will use the first found using the preference above.
requestHooks (array, optional) More info
Callbacks that are executed after the handler.
Note: Must be defined before or will throw.
response
Define response schema. It's applied only if the response send success: true
If the response is invalid a error response will be sent instead.
version (number) I plan to include this in the URL but can't find a proper way to do it. Any suggestions/issue!?
FQFN
F ully Q ualified F unction N ame. It's just a way to translate a string into function but requiring a module, not just by name.
"users.js:get" // tranlated into: require("users.js").get
"users.js:read.one" // tranlated into: require("users.js").read.one
// todo for future improvements
"users.js:!ret_get" // tranlated into: (new require("users.js")).ret_get
Handler default parameters (req, res, next)
Request (req)
Extend express.request
route
It's all the route JSON, here you can access your "data"primary
Boolean (always true atm) is an internal-request ?
Response (res)
Extend express.response
setResponse
(Object response)getResponse
() : ObjectaddError
(Number: http_status, String: message, Number: code, String: long_message)hasErrors
() : BooleanaddWarning
(String: message, Number: code, String: long_message)hasWarnings
(): Booleancontent
= {}variable where response, errors & warnings are stored. Use it with caution!
Do not use res.send unlike you really want it!
This is not the way to work with expressionist
(it's the express
way). You should use: setResponse
expressionist
is compatible with existing express
applications but encourage you to use another approach.
Callback (next)
it's the callback to continue with the execution. It's a good practice to always use "return next();", avoid executing twice the callback. Calling twice next could lead to many many problems. We will try to implement something to avoid it in the future.
Request Hooks.
This are callbacks called before the handler.
The most common example could be session management, authentication, extra validations (like object exists in database).
Request hooks can addError(s) to response object, in that case handler will not be executed.
Also the default behavior is to stop after a Hook add any number of errors, to prevent this use: "requestHooksPolicy: CONTINUE_ON_ERROR", handler will not be executed event with CONTINUE_ON_ERROR.
Example:
# define auth-hook
auth-hook:
type: requestHook
target: auth.js:preHook
doc: >
Require Authentication
# use auth-hook
private-zone:
type: uri
methods: [GET]
uri: /private/zone
doc: >
This example amuse that you don't send any parameter.
You will have many errors in the response
# function handler must exists
handler: private.js:get_zone
# continue to see both errors
requestHooksPolicy: CONTINUE_ON_ERROR
requestHooks:
- auth-hook
get: # GET
do_not_send_me:
cast: string
constraints:
length: [1, 32]
Response
{ success: false,
errors: [
{ code: 1000,
status: 400,
message: 'invalid-input',
long_message: '[do_not_send_me] is undefined' },
{ code: undefined,
status: 403,
message: 'invalid-auth',
long_message: undefined
}
]
}
Note: Response HTTP status code will be the one in the first error: 400. Expressionist do not remove status from errors because could be useful in some cases.
Response Hooks.
Same idea as requestHooks, but this time after the handler, so they have the response. Useful for response encoding, set headers, close connections to database, etc.
Documentation
expressionist.saveDoc("doc.html")
will generate:
- doc.html
- documentator.css
- documentator.html.js
example: doc.html
Client generation
usage: expresionist.getNodeClient(String base_url)
example:
# It's recommended to start IDs with the group sent in loadYML/JSON
# because the client is grouped by 'group'
users-read-one: #users.readOne
type: uri
methods: [GET]
uri: /users/:user_id
doc: >
Read user information
handler: users.js:read
params:
user_id:
cast: integer
expressionist.loadYML("users.yml", "users", function () {
});
var client = expresionist.nodeClient("http://www.mydomain.com");
client.users.readOne(1, {/*get*/}, {/*post*/}, function(err, response, body) {
});
Notes:
- Function name is camel case of the ID.
- First arguments are params, in the order defined
- Then GET/POST objects
- Then the callback
- everything is required
Why exceptions?
Expressionist will throw only when find an invalid input that the developer write. Any other error (user input) will add errors to response.
Log
expressionist use noboxout-log.
Mute log
expressionistInstance.logMute = true;
expressionistInstance.logTraces = 2; // how many stack item should be displayed
Adjust verbosity
expressionistInstance.logLevel = 5; // all
expressionistInstance.logLevel = 4; // no verbose
expressionistInstance.logLevel = 3; // no verbose, debug
expressionistInstance.logLevel = 2; // no verbose, debug, info
expressionistInstance.logLevel = 1; // no verbose, debug, info, warn
expressionistInstance.logLevel = 0; // no verbose, debug, info, warn, error