Meteor HTTP Factory
Create Meteor WebApp
(connect) HTTP middleware. Lightweight. Simple.
With this package you can define factory functions to create a variety of Meteor HTTP routes. Decouples definition from instantiation (also for the schema) and allows different configurations for different types of HTTP routes.
Minified size < 2KB!
Table of Contents
- Why do I want this?
- Installation
- Usage
- Responding with errors
- With schema
- Using middleware
- Codestyle - via npm - via Meteor npm
- Test - Watch mode
- Changelog
- License
Why do I want this?
- Decouple definition from instantiation
- Easy management between own and externally defined middleware on a local or global level
- Validate http request arguments (query/body) the same way as you do with
mdg:validated-method
- Just pass in the schema as plain object, instead of manually instantiating
SimpleSchema
- Easy builtin reponse schema, allowing you to either return a value (to create 200 responses) or throw an Error
(for 500 responses). You can still customize responses via req
, res
and next
.
- Easy data access and update between handlers using
this.data()
Installation
Simply add this package to your meteor packages
$ meteor add leaonline:http-factory
Usage
Import the createHTTPFactory
function and create the factory function from it.
The factory function can obtain the following arguments (*=optional):
path: String*
schema: Object*
- depends on, ifschemaFactory
is definedmethod: String*
- if defined, one of['get', 'head', 'post', 'put', 'delete', 'options', 'trace', 'patch']
validate: Function*
- if defined, a validation function that should throw an Error if validation failsrun: Function
- always required, the middleware handler to run on the current request
Basic example
To make life easier for you, the requests' query
or body
data is wrapped before the run
call into a universal
object. No need to directly access req.query
or req.body
and check for properties.
You can instead use the function
environment's data
method:
1import { createHTTPFactory } from 'meteor/leaonline:http-factory' 2const createHttpRoute = createHTTPFactory() // default, no params 3 4createHttpRoute({ 5 path: '/greetings', 6 run: function (/* req, res, next */) { 7 const { name } = this.data() // use this to get the current query/body data 8 return `Hello, ${name}` 9 } 10})
This code creates a http route, that is handled on any incoming HTTP request (get
, post
etc.) and assumes either in query
or on body (depending on request type) to find a parameter, named name
. Try it via the following client code:
1import { HTTP } from 'meteor/http' 2 3HTTP.get('/greetings', { params: { name: 'Ada' }}, (err, res) => { 4 console.log(res.content) // 'Hello, Ada' 5})
Use WebApp.rawConnectHandlers
If you need to define handlers before any other handler, just pass in the raw
option:
1import { createHTTPFactory } from 'meteor/leaonline:http-factory' 2const createHttpRoute = createHTTPFactory() // default, no params 3 4createHttpRoute({ 5 raw: true, 6 path: '/greetings', 7 run: function (/* req, res, next */) { 8 const { name } = this.data() 9 return `Hello, ${name}` 10 } 11})
Create universal handlers
You can omit path
on order to run the handler at the root level. This is often used for
middleware like cors
.
Specify a method
If you specify a HTTP method (one of ['get', 'head', 'post', 'put', 'delete', 'options', 'trace', 'patch']
) your
request will only be handled with the correct request method:
1import { WebApp } from 'meteor/webapp' 2import { createHTTPFactory } from 'meteor/leaonline:http-factory' 3import bodyParser from 'body-parser' 4 5WebApp.connectHandlers.urlEncoded(bodyParser /*, options */) // inject body parser 6 7const createHttpRoute = createHTTPFactory() // default, no params 8createHttpRoute({ 9 path: '/greetings', 10 method: 'post', 11 run: function (/* req, res, next */) { 12 const { name } = this.data() 13 return `Hello, ${name}` 14 } 15})
The data
will now contain the body
data. Note, that you may need to install npm body-parser
to
work with body content, that is not form data encoded.
Passing data to the next handler
We also made updating data much easier for you. You can pass an Object
to the this.data()
method in order to
attach new properties to a request or update existsing ones:
1import { WebApp } from 'meteor/webapp' 2import { createHTTPFactory } from 'meteor/leaonline:http-factory' 3 4const createHttpRoute = createHTTPFactory() // default, no params 5createHttpRoute({ 6 path: '/greetings', 7 method: 'get', 8 run: function (req, res, next ) { 9 const { name } = this.data() 10 const updateData = {} 11 if (name === 'Ada') { 12 updateData.title = 'Mrs.' 13 } 14 if (name === 'Bob') { 15 updateData.title = 'Mr.' 16 } 17 this.data(updateData) 18 next() 19 } 20}) 21 22createHttpRoute({ 23 path: '/greetings', 24 method: 'get', 25 run: function (/* req, res, next */) { 26 const { name, title } = this.data() 27 return `Hello, ${title} ${name}` 28 } 29})
If you call the route, it will contain now the updated data:
1import { HTTP } from 'meteor/http' 2 3HTTP.get('/greetings', { params: { name: 'Ada' }}, (err, res) => { 4 console.log(res.content) // 'Hello, Mrs. Ada' 5}) 6 7HTTP.get('/greetings', { params: { name: 'Bob' }}, (err, res) => { 8 console.log(res.content) // 'Hello, Mr. Ada' 9})
Responding with errors
If a requests is intended to return a fail / error response (400/500 types) you may use our simple solutions, that cover
most of the cases, while ensuring your run
code contains logic and not response handling.
Throwing 500 errors
If your run
method is throwing an Error, then it will be catched and transformed to a 500
response:
1import { createHTTPFactory } from 'meteor/leaonline:http-factory' 2const createHttpRoute = createHTTPFactory() // default, no params 3 4createHttpRoute({ 5 path: '/greetings', 6 run: function (/* req, res, next */) { 7 const { name } = this.data() 8 if (!name) throw new Error('Expected name') 9 return `Hello, ${name}` 10 } 11})
The err
param in the callback will then not be null
but contain the error response:
1import { HTTP } from 'meteor/http' 2 3HTTP.get('/greetings', {}, (err, res) => { 4 const error = err.response 5 console.log(error.statusCode) // 500 6 console.log(error.data.title) // 'Internal Server Error' 7 console.log(error.data.description) // 'An unintended error occurred.' 8 console.log(error.data.info) // Expected name 9})
Handle custom error responses
If you have a custom error response to return, you can use the builtin this.handleError
method:
1import { createHTTPFactory } from 'meteor/leaonline:http-factory' 2const createHttpRoute = createHTTPFactory() // default, no params 3 4createHttpRoute({ 5 path: '/greetings', 6 run: function (req, res, next) { 7 const data = this.data() 8 if (!data.name) { 9 return this.error({ 10 code: 400, 11 title: 'Bad Request', 12 description: 'Malformed query or body.' 13 }) 14 } 15 return `Hello, ${data.name}` 16 } 17})
With schema
In order to take the burden of input validation from you, we have added a nice schema
validation mechanism.
It works similar to the way mdg:validated-method
.
We support various ways to validate an input schema. To decouple schema definition from instantiation, we introduced a shemaFactory
, which
is basically a function that creates your schema for this collection. This also ensures, that
different HTTP routes don't share the same schema instances.
Using SimpleSchema
1import { createHTTPFactory } from 'meteor/leaonline:http-factory' 2import SimpleSchema from 'simpl-schema' 3 4const schemaFactory = definitions => new SimpleSchema(definitions) 5const createHttpRoute = createHTTPFactory({ schemaFactory }) 6 7createHttpRoute({ 8 path: '/greetings', 9 schema: { 10 name: String 11 }, 12 run: function (req, res, next) { 13 const { name } = this.data() 14 return `Hello, ${name}` 15 } 16})
Call the method via
1HTTP.get('/greetings', { params: { name: 'Ada' }}, (err, res) => { 2 console.log(res.content) // 'Hello, Ada' 3})
provoke a fail via
1HTTP.get('/greetings', (err, res) => { 2 const error = err.response 3 console.log(error.statusCode) // 400 4 console.log(error.data.title) // 'Bad request' 5 console.log(error.data.description) // 'Malformed query or body.' 6 console.log(error.data.info) // Name is required <-- SimpleSchema error message 7})
Overriding validate
when using schema
You can also override the internal validate
when using schema
by passing a validate
function.
This, however, disables the schema validation and is then your responsibility:
1import { createHTTPFactory } from 'meteor/leaonline:http-factory' 2import SimpleSchema from 'simpl-schema' 3 4const schemaFactory = definitions => new SimpleSchema(definitions) 5const createHttpRoute = createHTTPFactory({ schemaFactory }) 6 7createHttpRoute({ 8 path: '/greetings', 9 schema: { 10 name: String 11 }, 12 validate: () => {}, 13 run: function (/* req, res, next */) { 14 const { name } = this.data() 15 return `Hello, ${name}` 16 } 17})
and then call via
1HTTP.get('/greetings', (err, res) => { 2 console.log(res.content) // 'Hello, undefined' 3})
If none of these cover your use case, you can still create your own validation middleware.
Using check
You can also use Meteor's builtin check
and Match
for schema validation:
1import { check } from 'meteor/check' 2import { MyCollection } from '/path/to/MyCollection' 3import { createHTTPFactory } from 'meteor/leaonline:http-factory' 4 5const schemaFactory = schema => ({ 6 validate (args) { 7 check(args, schema) 8 } 9}) 10 11const createHttpRoute = createHTTPFactory({ schemaFactory }) 12createHttpRoute({ 13 path: '/greetings', 14 schema: { 15 name: String 16 }, 17 run: function (/* req, res, next */) { 18 const { name } = this.data() 19 return `Hello, ${name}` 20 } 21})
Note, that some definitions for SimpleSchema
and check
/Match
may differ.
Using middleware
Often you need to use third-party middle ware, such as cors
or jwt
. This package makes it
super easy to do so.
Define global middleware
First, you can define global middleware that is not bound to the factory environment,
which allows for highest compatibility.
Just define it with a property name, that is not one of schemaFactory, raw
:
1import { Meteor } from 'meteor/meteor' 2import { createHTTPFactory } from 'meteor/leaonline:http-factory' 3 4// is is just some simple example validation 5// of non-standard a-auth-token header 6const isValidToken = req => req.headers['x-auth-token'] === Meteor.settings.xAuthToken 7const simpleAuthExternal = function (req, res, next) { 8 if (!isValidToken(req)) { 9 // external middleware is neither bound to the environment 10 // nor affected in any way, so it can 100% maintin it's logic 11 // however, this.error is not available here 12 const body = JSON.stringify({ title: 'Permission Denied' }) 13 res.writeHead(403, { 'Content-Type': 'application/json' }) 14 res.end(body) 15 } 16 next() 17} 18 19// pass in this middleware on the abstract factory level 20// to make all routes of all methods to use this 21// additionally, use raw: true in order to ensure this is 22// run at the very first, before any package-level handlers 23const createHttpRoute = createHTTPFactory({ 24 simpleAuth: simpleAuthExternal, 25 raw: true 26}) 27 28createHttpRoute({ 29 path: '/greetings', 30 method: 'get', 31 run: function () { 32 const { name } = this.data() 33 return `Hello, ${name}` 34 } 35})
now your requests will run through this middleware:
1HTTP.get('/greetings', (err, res) => { 2 const error = err.response 3 console.log(error.statusCode) // 403 4 console.log(errpr.data.title) // 'Permission Denid' 5}) 6 7const params = { name: 'Ada' } 8const headers = { 'x-auth-token': Meteor.settings.xAuthToken } // warning: passing secrets to the client is unsafe 9HTTP.get('/greetings', { params, headers }, (err, res) => { 10 console.log(res.content) // Hello, Ada 11})
Define route-specific middleware
You can also define external middleware on a specific route without affecting other routes.
Just define it with a property name, that is not one of path, schema, method, run, validate
:
1import { Meteor } from 'meteor/meteor' 2import { createHTTPFactory } from 'meteor/leaonline:http-factory' 3import { simpleAuthExternal } from '/path/to/simpleAuthExternal' 4 5const createHttpRoute = createHTTPFactory() 6 7createHttpRoute({ 8 path: '/greetings', 9 simpleAuth: simpleAuthExternal, 10 method: 'get', 11 run: function () { 12 const { name } = this.data() 13 return `Hello, ${name}` 14 } 15})
It will work only on this route with this method, other routes won't be affected.
Define middleware using the internal environment
This becomes a bit redundant, but if you like to run middlware using the internal enviroment,
you need to place as the run
method:
1import { Meteor } from 'meteor/meteor' 2import { createHTTPFactory } from 'meteor/leaonline:http-factory' 3 4const createHttpRoute = createHTTPFactory() 5 6// is is just some simple example validation 7// of non-standard a-auth-token header 8const isValidToken = req => req.headers['x-auth-token'] === Meteor.settings.xAuthToken 9const simpleAuthInternal = function (req, res, next) { 10 if (!isValidToken(req)) { 11 // internally defined middleware can make use of the environment 12 return this.error({ 13 code: 403, 14 title: 'Permission Denied' 15 }) 16 } 17 next() 18} 19 20createHttpRoute({ 21 path: '/greetings', 22 method: 'get', 23 run: simpleAuthInternal 24}) 25 26createHttpRoute({ 27 path: '/greetings', 28 method: 'get', 29 run: function () { 30 const { name } = this.data() 31 return `Hello, ${name}` 32 } 33})
Hooks
You can currently only hook into error handling:
1import { Meteor } from 'meteor/meteor' 2import { createHTTPFactory } from 'meteor/leaonline:http-factory' 3 4const createHttpRoute = createHTTPFactory({ 5 // global error hook for all routes 6 onError: e => { 7 MyCoolLogger.error(e) 8 } 9}) 10 11createHttpRoute({ 12 path: '/greetings', 13 method: 'get', 14 run: simpleAuthInternal, 15 // local error hook only for this route, note 16 // that this route will now use only this hook and not 17 // the global hook, if such is defined 18 onError: e => { 19 MyCoolLogger.error(e) 20 } 21}) 22 23createHttpRoute({ 24 path: '/greetings', 25 method: 'get', 26 run: function () { 27 const { name } = this.data() 28 return `Hello, ${name}` 29 } 30})
Codestyle
We use standard
as code style and for linting.
via npm
$ npm install --global standard snazzy $ standard | snazzy
via Meteor npm
$ meteor npm install --global standard snazzy $ standard | snazzy
Test
We use meteortesting:mocha
to run our tests on the package.
Watch mode
$ TEST_WATCH=1 TEST_CLIENT=0 meteor test-packages ./ --driver-package meteortesting:mocha
Changelog
- 1.1.0
- fix: tests when run method returns undefined values or no value (=undefined)
- feature:
onError
hook can be attached to global factory and factories
- 1.0.1
- use
EJSON
to stringify results in order to comply with any formats, that can be resolved via EJSON
- use
License
MIT, see LICENSE