UNPKG

43.8 kBMarkdownView Raw
1# Ravel
2> Forge past a tangle of modules. Make a cool app.
3
4[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/raveljs/ravel/master/LICENSE) [![npm version](https://badge.fury.io/js/ravel.svg)](http://badge.fury.io/js/ravel) [![Dependency Status](https://david-dm.org/raveljs/ravel.svg)](https://david-dm.org/raveljs/ravel) [![npm](https://img.shields.io/npm/dm/ravel.svg?maxAge=2592000)](https://www.npmjs.com/package/ravel) [![Build Status](https://travis-ci.org/raveljs/ravel.svg?branch=master)](https://travis-ci.org/raveljs/ravel) [![Build status](https://ci.appveyor.com/api/projects/status/5kx5j2d1fhyn9yn3/branch/master?svg=true)](https://ci.appveyor.com/project/Ghnuberath/ravel/branch/master) [![Code Climate](https://codeclimate.com/github/raveljs/ravel/badges/gpa.svg)](https://codeclimate.com/github/raveljs/ravel) [![Test Coverage](https://codeclimate.com/github/raveljs/ravel/badges/coverage.svg)](https://codeclimate.com/github/raveljs/ravel/coverage) [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg?style=flat-square)](https://github.com/Flet/semistandard)
5
6Ravel is a tiny, sometimes-opinionated foundation for creating organized, maintainable, and scalable web applications in [node.js](https://github.com/joyent/node) with [ES2016/2017](http://kangax.github.io/compat-table/esnext/).
7
8**Note:** The `master` branch may be in an unstable or even broken state during development. Please use [releases](https://github.com/raveljs/ravel/releases) instead of the `master` branch to view stable code.
9
10## Table of Contents
11
12<!-- TOC depthFrom:2 depthTo:3 withLinks:1 updateOnSave:1 orderedList:0 -->
13
14- [Table of Contents](#table-of-contents)
15- [Introduction](#introduction)
16- [Installation](#installation)
17- [Architecture](#architecture)
18 - [Modules (and Errors)](#modules-and-errors)
19 - [Routes](#routes)
20 - [Resources](#resources)
21 - [Bringing it all together](#bringing-it-all-together)
22 - [Decorator Transpilation](#decorator-transpilation)
23 - [Running the Application](#running-the-application)
24- [API Documentation](#api-documentation)
25 - [Ravel App](#ravel-app)
26 - [Managed Configuration System](#managed-configuration-system)
27 - [Ravel.Error](#ravelerror)
28 - [Ravel.Module](#ravelmodule)
29 - [Ravel.Routes](#ravelroutes)
30 - [Ravel.Resource](#ravelresource)
31 - [Response Caching](#response-caching)
32 - [Database Providers](#database-providers)
33 - [Transaction-per-request](#transaction-per-request)
34 - [Scoped Transactions](#scoped-transactions)
35 - [Authentication Providers](#authentication-providers)
36 - [Authentication](#authentication)
37- [Deployment and Scaling](#deployment-and-scaling)
38
39<!-- /TOC -->
40
41## Introduction
42
43Ravel is inspired by the simplicity of [koa](http://koajs.com/) and [express](http://expressjs.com), but aims to provide a pre-baked, well-tested and highly modular solution for creating enterprise web applications by providing:
44
45- A standard set of well-defined architectural components so that your code stays **organized**
46- Rapid **REST API** definition
47- Easy **bootstrapping** via an enforced, reference configuration of [koa](http://koajs.com/) with critical middleware
48- Dependency injection (instead of relative `require`s)
49
50And a few other features, plucked from popular back-end frameworks:
51
52- Transaction-per-request
53- Simple authentication and authentication configuration (no complex [passport](https://github.com/jaredhanson/passport) setup)
54- Externalized session storage for horizontal scalability
55
56Ravel is layered on top of awesome technologies, including:
57- [koa](http://koajs.com/)
58- [Passport](https://github.com/jaredhanson/passport)
59- [Intel](https://github.com/seanmonstar/intel)
60- [Redis](https://github.com/antirez/redis)
61- [docker](http://docker.com)
62
63
64## Installation
65
66> As Ravel uses async/await and several other ES2015/2016 features, you will need to use a 7.6.x+ distribution of node
67
68```bash
69$ npm install ravel
70```
71
72Ravel also relies on [Redis](https://github.com/antirez/redis). If you don't have it installed and running, try using [docker](docker.com) to quickly spin one up:
73
74```bash
75$ docker run -d -p 6379:6379 redis
76```
77
78## Architecture
79
80Ravel applications consist of a few basic parts:
81
82- **Modules:** plain old classes which offer a great place to write modular application logic, middleware, authentication logic, etc.
83- **Routes:** a low-level place for general routing logic
84- **Resources:** built on top of `Routes`, `Resource`s are REST-focused
85- **Errors:** Node.js `Error`s which are associated with an HTTP response code. `throw` them or `reject` with them and `Routes` and `Resource`s will respond accordingly
86
87If you're doing it right, your applications will consist largely of `Module`s, with a thin layer of `Routes` and `Resource`s on top.
88
89### Modules (and Errors)
90
91`Module`s are plain old node.js modules exporting a single class which encapsulates application logic. `Module`s support dependency injection of core Ravel services and other Modules alongside npm dependencies *(no relative `require`'s!)*. `Module`s are instantiated safely in dependency-order, and cyclical dependencies are detected automatically.
92
93For more information about `Module`s, look at [Ravel.Module](#ravelmodule) below.
94
95*modules/cities.js*
96```javascript
97const Ravel = require('ravel');
98const Error = Ravel.Error;
99const Module = Ravel.Module;
100const inject = Ravel.inject;
101
102/**
103 * First, we'll define an Error we will throw when a requested
104 * city is not found. This Error will be associated with the
105 * HTTP error code 404.
106 */
107class MissingCityError extends Error {
108 constructor (name) {
109 super(`City ${name} does not exist.`, Ravel.httpCodes.NOT_FOUND);
110 }
111}
112
113/**
114 * Our main Module, defining logic for working with Cities
115 */
116@inject('moment')
117class Cities extends Module {
118 constructor (moment) {
119 super();
120 this.moment = moment;
121 this.cities = ['Toronto', 'New York', 'Chicago']; // our fake 'database'
122 }
123
124 getAllCities () {
125 return Promise.resolve(this.cities);
126 }
127
128 getCity (name) {
129 return new Promise((resolve, reject) => {
130 const index = this.cities.indexOf(name);
131 if (index !== -1) {
132 resolve(this.cities[index]);
133 } else {
134 // Ravel will automatically respond with the appropriate HTTP status code!
135 this.log.warn(`User requested unknown city ${name}`);
136 reject(new MissingCityError(name));
137 }
138 });
139 }
140}
141
142// Export Module class
143module.exports = Cities;
144```
145
146### Routes
147
148`Routes` are `Ravel`'s lower-level wrapper for `koa` (`Resource`s are the higher-level one). They support GET, POST, PUT and DELETE requests, and middleware, via decorators. Like `Module`s, they also support dependency injection. Though `Routes` can do everything `Resources` can do, they are most useful for implementing non-REST things, such as static content serving or template serving (EJS, Jade, etc.). If you want to build a REST API, use `Resource`s instead (they're up next!).
149
150For more information about `Routes`, look at [Ravel.Routes](#ravelroutes) below.
151
152*routes/index.js*
153```javascript
154const Ravel = require('ravel');
155const Routes = Ravel.Routes;
156const inject = Ravel.inject;
157const before = Routes.before; // decorator to add middleware to an endpoint within the Routes
158const mapping = Routes.mapping; // decorator to associate a handler method with an endpoint
159
160@inject('middleware1') // middleware from NPM, or your own modules, etc.
161class ExampleRoutes extends Routes {
162 constructor (middleware1) {
163 super('/'); // base path for all routes in this class. Will be prepended to the @mapping.
164 this.middleware1 = middleware1;
165 // you can also build middleware right here!
166 this.middleware2 = async function (next) {
167 await next;
168 };
169 }
170
171 // bind this method to an endpoint and verb with @mapping. This one will become GET /app
172 @mapping(Routes.GET, 'app')
173 @before('middleware1','middleware2') // use @before to place middleware before appHandler
174 async appHandler (ctx) {
175 // ctx is just a koa context! Have a look at the koa docs to see what methods and properties are available.
176 ctx.body = '<!DOCTYPE html><html><body>Hello World!</body></html>';
177 ctx.status = 200;
178 }
179}
180
181// Export Routes class
182module.exports = ExampleRoutes;
183```
184
185### Resources
186
187What might be referred to as a *controller* in other frameworks, a `Resource` module defines HTTP methods on an endpoint, supporting the session-per-request transaction pattern via Ravel middleware. `Resource`s also support dependency injection, allowing for the easy creation of RESTful interfaces to your `Module`-based application logic. Resources are really just a thin wrapper around `Routes`, using specially-named handler functions (`get`, `getAll`, `post`, `put`, `putAll`, `delete`, `deleteAll`) instead of `@mapping`. This convention-over-configuration approach makes it easier to write proper REST APIs with less code, and is recommended over "carefully chosen" `@mapping`s in a `Routes` class.
188
189For more information about `Resource`s, look at [Ravel.Resource](#ravelresouce) below.
190
191*resources/city.js*
192```javascript
193// Resources support dependency injection too!
194// Notice that we have injected our cities Module by name.
195const Ravel = require('ravel');
196const Resource = Ravel.Resource;
197const inject = Ravel.inject;
198const before = Resource.before; // decorator to add middleware to an endpoint within the Resource
199
200// using @before at the class level decorates all endpoint methods with middleware
201@inject('cities')
202class CitiesResource extends Resource {
203 constructor (cities) {
204 super('/cities'); //base path
205 this.cities = cities;
206
207 // some other middleware, which you might have injected from a Module or created here
208 this.anotherMiddleware = async function (next) {
209 await next;
210 };
211 }
212
213 // no need to use @mapping here. Routes methods are automatically mapped using their names.
214 async getAll (ctx) { // just like in Routes, ctx is a koa context.
215 ctx.body = await this.cities.getAllCities();
216 }
217
218 @before('anotherMiddleware') // using @before at the method level decorates this method with middleware
219 async get (ctx) { // get routes automatically receive an endpoint of /cities/:id (in this case).
220 ctx.body = await this.cities.getCity(ctx.params.id);
221 }
222
223 // post, put, putAll, delete and deleteAll are
224 // also supported. Not specifying them for
225 // this resource will result in calls using
226 // those verbs returning HTTP 501 NOT IMPLEMENTED
227
228 // postAll is not supported, because it makes no sense
229}
230
231// Export Resource class
232module.exports = CitiesResource;
233```
234
235### Bringing it all together
236
237*app.js*
238```javascript
239const app = new require('ravel')();
240
241// parameters like this can be supplied via a .ravelrc.json file
242app.set('keygrip keys', ['mysecret']);
243
244app.modules('./modules'); //import all Modules from a directory
245app.resources('./resources'); //import all Resources from a directory
246app.routes('./routes/index.js'); //import all Routes from a file
247
248// start it up!
249app.start();
250```
251
252### Decorator Transpilation
253
254Since decorators are not yet available in Node, you will need to use a transpiler to convert them into ES2016-compliant code. We have chosen [Babel](https://babeljs.io/) as our recommended transpiler.
255
256```bash
257$ npm install gulp-sourcemaps@1.6.0 babel-core@6.18.2 babel-plugin-transform-decorators-legacy@1.3.4 gulp-babel@6.1.2
258# Note, please add babel-plugin-transform-async-to-generator@6.16.0 if you are using Node v6 instead of v7.
259```
260
261*gulpfile.js*
262```js
263const babelConfig = {
264 'retainLines': true,
265 'plugins': ['transform-decorators-legacy'] // add 'transform-async-to-generator' if you are using Node v6 instead of v7
266};
267gulp.task('transpile', function () {
268 return gulp.src('src/**/*.js') // point it at your source directory, containing Modules, Resources and Routes
269 .pipe(plugins.sourcemaps.init())
270 .pipe(plugins.babel(babelConfig))
271 .pipe(plugins.sourcemaps.write('.'))
272 .pipe(gulp.dest('dist')); // your transpiled Ravel app will appear here!
273});
274```
275
276Check out the [starter project](https://github.com/raveljs/ravel-github-mariadb-starter) to see a working example of this build process.
277
278### Running the Application
279
280```bash
281$ node dist/app.js
282```
283
284## API Documentation
285> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/)
286
287### Ravel App
288> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/ravel.js.html)
289
290A Ravel application is a root application file (such as `app.js`), coupled with a collection of files exporting `Module`s, `Resource`s and `Routes` (see [Architecture](#architecture) for more information). Getting started is usually as simple as creating `app.js`:
291
292*app.js*
293```js
294const Ravel = require('ravel');
295const app = new Ravel();
296
297(async () => {
298 // you'll register managed parameters, and connect Modules, Resources and Routes here
299 await app.init();
300 // you'll set managed parameters here
301 await app.listen();
302})();
303```
304
305### Managed Configuration System
306> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/core/params.js.html)
307
308Traditional `node` appliations often rely on `process.env` for configuration. This can lead to headaches when an expected value is not declared in the environment, a value is supplied but doesn't match any expected ones, or the name of an environment variable changes and refactoring mistakes are made. To help mitigate this common issue, Ravel features a simple configuration system which relies on three methods:
309
310#### app.registerParameter
311> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/core/params.js.html#registerParameter)
312
313Create managed parameters with `app.registerParameter()`:
314
315*app.js*
316```js
317const Ravel = require('ravel');
318const app = new Ravel();
319
320// register a new optional parameter
321app.registerParameter('my optional parameter');
322// register a new required parameter
323app.registerParameter('my required parameter', true);
324// register a required parameter with a default value
325app.registerParameter('my third parameter', true, 'some value');
326
327(async () => {
328 await app.init();
329 await app.listen();
330})();
331```
332
333Many Ravel plugin libraries will automatically create parameters which you will have to supply values for. These parameters will be documented in their `README.md`.
334
335#### app.set
336> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/core/params.js.html#set)
337
338Provide values via `app.set()`. Setting an unknown parameter will result in an `Error`.
339
340*app.js*
341```js
342const Ravel = require('ravel');
343const app = new Ravel();
344
345// register a new optional parameter
346app.registerParameter('my optional parameter');
347
348(async () => {
349 await app.init();
350
351 // set a value
352 app.set('my optional parameter', 'some value');
353 // this won't work:
354 app.set('an unknown parameter', 'some value');
355
356 await app.listen();
357})();
358```
359
360#### app.get
361> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/core/params.js.html#get)
362
363Retrieve values via `app.get()`. Retrieving an unknown parameter will result in an `Error`.
364
365*app.js*
366```js
367const Ravel = require('ravel');
368const app = new Ravel();
369
370// register a new parameter
371app.registerParameter('my required parameter', true, 'default value');
372
373(async () => {
374 await app.init();
375
376 // set a value
377 app.set('my required parameter', 'some value');
378 // get a value
379 app.get('my required parameter') === 'some value';
380 // this won't work:
381 // app.get('an unknown parameter');
382
383 await app.listen();
384})();
385```
386
387#### Core parameters
388
389Ravel has several core parameters:
390
391```js
392// you have to set these:
393app.set('keygrip keys', ['my super secret key']);
394
395// these are optional (default values are shown):
396app.set('redis host', '0.0.0.0');
397app.set('redis port', 6379);
398app.set('redis password', undefined);
399app.set('redis max retries', 10); // connection retries
400app.set('port', 8080); // port the app will run on
401app.set('session key', 'koa.sid'); // the cookie name to use for sessions
402app.set('session max age', null); // session maxAge (default never expires)
403app.set('app route', '/'); // if you have a UI, this is the path users will be sent to when they are logged in
404app.set('login route', '/login'); // if users aren't logged in and you redirect them, this is where they'll be sent
405app.set('koa public directory', undefined); // if you want to statically serve a directory
406app.set('koa view directory', undefined); // for templated views (EJS, Pug, etc.)
407app.set('koa view engine', undefined); // for templated views (EJS, Pug, etc.)
408app.set('koa favicon path', undefined); // favicon middleware configuration
409```
410
411#### .ravelrc.json
412
413To make it easier to supply configuration values to Ravel, a `.ravelrc.json` file can be placed beside `app.js` (or in any parent directory of `app.js`). This is the recommended method of setting parameters, with the exception of ones derived from `process.env` (which would need to be set programmatically).
414
415*.ravelrc.json*
416```
417{
418 "keygrip keys": ["my super secret key"]
419}
420```
421
422### Ravel.Error
423> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/util/application_error.js.html)
424
425This is the base `Error` type for Ravel, meant to be extended into semantic errors which can be used within your applications. When you create a custom `Ravel.Error`, you **must** provide an associated HTTP status code, which Ravel will automatically respond with if an HTTP request results in that particular `Error` being thrown. This helps create meaningful status codes for your REST APIs while working within traditional `node` error-handling paradigms (`throw/try/catch` and `Promise.reject()`). Errors are generally best-declared within `Module`, `Resource` or `Routes` files (and not exported), closest to where they are used.
426
427*at the top of some `Module`, `Resource` or `Routes` file (we'll get to this next)*
428```js
429const Ravel = require('ravel');
430/**
431 * Thrown when a user tries to POST something unexpected to /upload
432 */
433class UploadError extends Ravel.Error {
434 constructor (msg) {
435 super(msg, Ravel.httpCodes.BAD_REQUEST);
436 }
437}
438```
439
440### Ravel.Module
441> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/core/module.js.html)
442
443`Module`s are meant to contain the bulk of your application logic, either to support endpoints defined in `Resource`s and `Routes`, or to perform tasks at specific points during the Ravel lifecycle (see [Lifecycle Decorators](#lifecycle-decorators) below).
444
445Here's a simple module:
446
447*modules/my-module.js*
448```js
449const Ravel = require('ravel');
450const inject = Ravel.inject; // Ravel's dependency injection decorator
451const Module = Ravel.Module; // base class for Ravel Modules
452
453// inject a custom ravel Module (or your plain classes) beside npm dependencies!
454@inject('path', 'fs', 'custom-module', 'plain-class')
455class MyModule extends Module {
456 constructor (path, fs, custom, plain) { // @inject'd modules are available here as parameters
457 super();
458 this.path = path;
459 this.fs = fs;
460 this.custom = custom;
461 this.plain = plain;
462 }
463
464 // implement any methods you like :)
465 aMethod () {
466 // ...
467 }
468
469 async anAsyncMethod () {
470 // ...
471 }
472}
473
474module.exports = MyModule; // you must export your Module so that Ravel can require() it.
475```
476
477#### Dependency Injection and Module Registration
478> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/core/decorators/inject.js.html)
479
480Ravel's *dependency injection* system is meant to address several issues with traditional `require()`s:
481
482- Using `require()` with one's own modules in a complex project often results in statements like this: `require('../../../../my/module');`. This issue is especially pronounced when `require()`ing source modules in test files.
483- Cyclical dependencies between modules are not always obvious in a large codebase, and can result in unexpected behaviour.
484
485Ravel addresses this with the the [`@inject`](http://raveljs.github.io/docs/latest/core/decorators/inject.js.html) decorator:
486
487*modules/my-module.js*
488```js
489const Ravel = require('ravel');
490const inject = Ravel.inject;
491const Module = Ravel.Module;
492
493@inject('another-module') // inject another Module from your project without require()!
494class MyModule extends Module {
495 constructor (another) { // @inject'd modules are available here as parameters
496 super();
497 this.another = another;
498 }
499}
500module.exports = MyModule;
501```
502
503The injection name of `another-module` comes from its filename, and can be overriden in `app.js`:
504
505*app.js*
506```js
507// ...
508const app = new Ravel();
509// the first argument is the path to the module file.
510// the second is the name you assign for dependency injection.
511app.module('./modules/my-module', 'my-module');
512app.module('./modules/another-module', 'another-module');
513// assigning names manually becomes tedious fast, so Ravel can
514// infer the names from the names of your files when you use
515// app.modules to scan a directory:
516app.modules('./modules'); // this would register modules with the same names as above
517```
518
519`Module`s are singletons which are instantiated in *dependency-order* (i.e. if `A` depends on `B`, `B` is guaranteed to be constructed first). Cyclical dependencies are detected automatically and result in an `Error`.
520
521`app.module`, `app.modules` and `@inject` also work on files exporting plain classes which do not extend `Ravel.Module`. This makes it easier to create and/or use simple, plain classes which do not need access to the full Ravel framework (i.e. `this.log`, `this.ApplicationError`, etc.).
522
523To further simplify working with imports in Ravel, you can `@inject` core `node` modules and `npm` dependencies (installed in your local `node_modules` or globally) alongside your own `Module`s:
524
525```js
526const Ravel = require('ravel');
527const inject = Ravel.inject;
528const Module = Ravel.Module;
529
530@inject('another-module', 'path', 'moment') // anything that can be require()d can be @injected
531class MyModule extends Module {
532 constructor (another, path, moment) {
533 super();
534 // ...
535 }
536}
537module.exports = MyModule;
538```
539
540#### Module Namespacing
541
542In a large project, it may become desirable to namespace your `Module`s to avoid naming conflicts. This is easily accomplished with Ravel by separating source files for `Module`s into different directories. Let's assume the following project structure:
543
544```
545app.js
546.ravelrc.json
547modules/
548 core/
549 my-module.js
550 util/
551 my-module.js
552```
553
554Then, import the `Module` directory as before, using `app.modules()`:
555
556*app.js*
557```js
558// ...
559const app = new Ravel();
560app.modules('./modules');
561// core/my-module can now be injected using @inject(core.my-module)!
562// util/my-module can now be injected using @inject(util.my-module)!
563```
564
565> Essentially, Ravel ignores the path you pass to `app.modules()` and uses any remaining path components to namespace `Module`s.
566
567#### Lifecycle Decorators
568> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/core/decorators/lifecycle.js.html)
569
570`Module`s are also a great place to define logic which should run at particular points during the Ravel lifecycle. Decorating a `Module` method with a lifecycle decorator appropriately results in that method firing exactly once at the specified time (with the exception of `@interval`, of course):
571
572```js
573const Ravel = require('ravel');
574const Module = Ravel.Module;
575const prelisten = Module.prelisten;
576
577class MyInitModule extends Module {
578 // ...
579 @prelisten
580 initDBTables () {
581 // ...
582 }
583}
584module.exports = MyInitModule;
585```
586
587There are currently six lifecycle decorators:
588
589- `@postinit` fires at the end of `Ravel.init()`
590- `@prelisten` fires at the beginning of `Ravel.listen()`
591- `@postlisten` fires at the end of `Ravel.listen()`
592- `@preclose` fires at the beginning of `Ravel.close()`
593- `@interval(1000)` fires at the end of `Ravel.listen()` and then repeatedly at the specified interval until `Ravel.close()`
594- `@koaconfig` fires during `Ravel.init()`, after Ravel is finished configuring the underlying `koa` app object with global middleware. Methods decorated with `@koaconfig` receive a reference to the underlying `koa` app object for customization. This decorator is meant for exceptional circumstances, since (unnecessarily) global middleware constitutes a hot path and can lead to inefficiency.
595
596### Ravel.Routes
597> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/core/routes.js.html)
598
599`Routes` are Ravel's abstraction of `koa`. They provide Ravel with a simple mechanism for registering `koa` routes, which should (generally) only be used for serving templated pages or static content (not for building RESTful APIs, for which `Ravel.Resource` is more applicable). Extend this abstract superclass to create a `Routes` module.
600
601Like `Module`s, `Routes` classes support dependency injection, allowing easy connection of application logic and web layers.
602
603Endpoints are created within a `Routes` class by creating an `async` method and then decorating it with [`@mapping`](http://raveljs.github.io/docs/latest/core/decorators/mapping.js.html). The `@mapping` decorator indicates the path for the route (concatenated with the base path passed to `super()` in the `constructor`), as well as the HTTP verb. The method handler accepts a single argument `ctx` which is a [koa context](http://koajs.com/#context). Savvy readers with `koa` experience will note that, within the handler, `this` refers to the instance of the Routes class (to make it easy to access injected `Module`s), and the passed `ctx` argument is a reference to the `koa` context.
604
605*routes/my-routes.js*
606```js
607const inject = require('ravel').inject;
608const Routes = require('ravel').Routes;
609const mapping = Routes.mapping; // Ravel decorator for mapping a method to an endpoint
610const before = Routes.before; // Ravel decorator for conneting middleware to an endpoint
611
612// you can inject your own Modules and npm dependencies into Routes
613@inject('koa-bodyparser', 'fs', 'custom-module')
614class MyRoutes extends Routes {
615 // The constructor for a `Routes` class must call `super()` with the base
616 // path for all routes within that class. Koa path parameters such as
617 // :something are supported.
618 constructor (bodyParser, fs, custom) {
619 super('/'); // base path for all routes in this class
620 this.bodyParser = bodyParser(); // make bodyParser middleware available
621 this.fs = fs;
622 this.custom = custom;
623 }
624
625 // will map to GET /app
626 @mapping(Routes.GET, 'app'); // Koa path parameters such as :something are supported
627 @before('bodyParser') // use bodyParser middleware before handler. Matches this.bodyParser created in the constructor.
628 async appHandler (ctx) {
629 ctx.status = 200;
630 ctx.body = '<!doctype html><html></html>';
631 // ctx is a koa context object.
632 // await on Promises and use ctx to create a body/status code for response
633 // throw a Ravel.Error to automatically set an error status code
634 }
635}
636
637module.exports = MyRoutes;
638```
639
640#### Registering Routes
641
642Much like `Module`s, `Routes` can be added to your Ravel application via `app.routes('path/to/routes')`:
643
644*app.js*
645```js
646// ...
647const app = new Ravel();
648// you must add routes one at a time. Directory scanning is not supported.
649app.routes('./routes/my-routes');
650```
651
652### Ravel.Resource
653> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/core/resource.js.html)
654
655What might be referred to as a *controller* in other frameworks, a `Resource` module defines HTTP methods on an endpoint. `Resource`s also support dependency injection, allowing for the easy creation of RESTful interfaces to your `Module`-based application logic. Resources are really just a thin wrapper around `Routes`, using specially-named handler methods (`get`, `getAll`, `post`, `put`, `putAll`, `delete`, `deleteAll`) instead of `@mapping`. This convention-over-configuration approach makes it easier to write proper REST APIs with less code, and is recommended over ~~carefully chosen~~ `@mapping`s in a `Routes` class. Omitting any or all of the specially-named handler functions is fine, and will result in a `501 NOT IMPLEMENTED` status when that particular method/endpoint is requested. `Resource`s inherit all the properties, methods and decorators of `Routes`. See [core/routes](routes.js.html) for more information. Note that `@mapping` does not apply to `Resources`.
656
657As with `Routes` classes, `Resource` handler methods are `async` functions which receive a [koa context](http://koajs.com/#context) as their only argument.
658
659*resources/person-resource.js*
660```js
661const inject = require('ravel').inject;
662const Resource = require('ravel').Resource;
663const before = Routes.before;
664
665// you can inject your own Modules and npm dependencies into Resources
666@inject('koa-bodyparser', 'fs', 'custom-module')
667class PersonResource extends Resource {
668 constructor(convert, bodyParser, fs, custom) {
669 super('/person'); // base path for all routes in this class
670 this.bodyParser = bodyParser(); // make bodyParser middleware available
671 this.fs = fs;
672 this.custom = custom;
673 }
674
675 // will map to GET /person
676 @before('bodyParser') // use bodyParser middleware before handler
677 async getAll (ctx) {
678 // ctx is a koa context object.
679 // await on Promises, and set ctx.body to create a body for response
680 // "OK" status code will be chosen automatically unless configured via ctx.status
681 // Extend and throw a Ravel.Error to send an error status code
682 }
683
684 // will map to GET /person/:id
685 async get (ctx) {
686 // can use ctx.params.id in here automatically
687 }
688
689 // will map to POST /person
690 async post (ctx) {}
691
692 // will map to PUT /person
693 async putAll (ctx) {}
694
695 // will map to PUT /person/:id
696 async put (ctx) {}
697
698 // will map to DELETE /person
699 async deleteAll (ctx) {}
700
701 // will map to DELETE /person/:id
702 async delete (ctx) {}
703}
704
705module.exports = PersonResource;
706```
707
708#### Registering Resources
709> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/core/resources.js.html)
710
711Much like `Module`s, `Resource`s can be added to your Ravel application via `app.resources('path/to/resources/directory')`:
712
713*app.js*
714```js
715// ...
716const app = new Ravel();
717// directory scanning!
718app.resources('./resources');
719```
720
721### Response Caching
722
723Ravel supports transparent response caching via the `@cache` decorator, which can be applied at both the class and method-level of `Resource`s and `Routes`. Method-level applications of `@cache` override class-level ones.
724
725*Method-level example*
726```js
727const Routes = require('ravel').Routes;
728const mapping = Routes.mapping;
729const cache = Routes.cache;
730
731class MyRoutes extends Routes {
732 constructor () {
733 super('/');
734 }
735
736 @cache // method-level version only applies to this route
737 @mapping(Routes.GET, '/projects/:id')
738 async handler (ctx) {
739 // The response will automatically be cached when this handler is run
740 // for the first time, and then will be served instead of running the
741 // handler for as long as the cached response is available.
742 // If this handler throws an exception, then that response will not be cached.
743 }
744}
745```
746
747*Class-level example, with options*
748```js
749const Resource = require('ravel').Resource;
750const cache = Resource.cache;
751
752// class-level version applies to all routes in class, overriding any
753// method-level instances of the decorator.
754@cache({expire:60, maxLength: 100}) // expire is measured in seconds. maxLength in bytes.
755class MyResource extends Resource {
756 constructor (bodyParser) {
757 super('/');
758 this.bodyParser = bodyParser();
759 }
760
761 async get(ctx) {
762 // The response will automatically be cached when this handler is run
763 // for the first time, and then will be served instead of running the
764 // handler for as long as the cached response is available (60 seconds).
765 }
766}
767```
768
769### Database Providers
770> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/db/database_provider.js.html)
771
772A `DatabaseProvider` is a lightweight wrapper for a `node` database library (such as [node-mysql](https://github.com/felixge/node-mysql)) which performs all the complex set-up and configuration of the library automatically, and registers simple parameters which you must `app.set` (such as the database host ip). The true purpose of `DatabaseProvider`s is to reduce boilerplate code between applications, as well as facilitate Ravel's transaction-per-request system (coming up [next](#transaction-per-request)). You may use as many different `DatbaseProvider`s as you wish in your application. Here's an example pulled from [`ravel-mysql-provider`](https://github.com/raveljs/ravel-mysql-provider):
773
774#### Example Setup
775
776*app.js*
777```javascript
778const app = new require('ravel')();
779const MySQLProvider = require('ravel-mysql-provider');
780new MySQLProvider(app, 'mysql');
781// ... other providers and parameters
782(async () => {
783 await app.init();
784})();
785// ... the rest of your Ravel app
786```
787
788#### Example Configuration
789
790*.ravelrc.json*
791```json
792{
793 "mysql options": {
794 "host": "localhost",
795 "port": 3306,
796 "user": "root",
797 "password": "a password",
798 "database": "mydatabase",
799 "idleTimeoutMillis": 5000,
800 "connectionLimit": 10
801 }
802}
803```
804
805#### List of Ravel `DatabaseProvider`s
806
807Ravel currently supports several `DatabaseProvider`s via external libraries.
808
809 - [`ravel-mysql-provider`](https://github.com/raveljs/ravel-mysql-provider)
810 - [`ravel-rethinkdb-provider`](https://github.com/raveljs/ravel-rethinkdb-provider)
811 - [`ravel-neo4j-provider`](https://github.com/raveljs/ravel-neo4j-provider)
812
813> If you've written a `DatabaseProvider` and would like to see it on this list, contact us or open an issue/PR against this README!
814
815### Transaction-per-request
816> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/db/decorators/transaction.js.html)
817
818The `@transaction` decorator is Ravel's way of automatically opening (and managing) database connections for a `Routes` or `Resource` handler method. It is available for import as `Routes.transaction` or `Resource.transaction`.
819
820When used at the method-level, `@transaction` opens connections for that specific handler method. When used at the class-level, it open connections for all handler methods in that `Route` or `Resource` class.
821
822Connections are available within the handler method as an object `ctx.transaction`, which contains connections as values and `DatabaseProvider` names as keys. Connections will be closed automatically when the endpoint responds (**do not close them yourself**), and will automatically roll-back changes if a `DatabaseProvider` supports it (generally a SQL-only feature).
823
824*resources/person-resource.js*
825```js
826const Resource = require('ravel').Resource;
827const transaction = Resource.transaction;
828
829class PersonResource extends Resource {
830 constructor (bodyParser, fs, custom) {
831 super('/person');
832 }
833
834 // maps to GET /person/:id
835 @transaction('mysql') // this is the name exposed by ravel-mysql-provider
836 async get (ctx) {
837 // TIP: Don't write complex logic here. Pass ctx.transaction into
838 // a Module function which returns a Promise! This example is
839 // just for demonstration purposes.
840 ctx.body = await new Promise((resolve, reject) => {
841 // ctx.transaction.mysql is a https://github.com/felixge/node-mysql connection
842 ctx.transaction.mysql.query('SELECT 1', (err, rows) => {
843 if (err) return reject(err);
844 resolve(rows);
845 });
846 });
847 }
848}
849module.exports = PersonResource;
850```
851
852### Scoped Transactions
853> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/core/module.js.html)
854
855Sometimes, you may need to open a transaction outside of a code path triggered by an HTTP request. Good examples of this might include database initialization at application start-time, or logic triggered by a websocket connection. In these cases, a `Module` class can open a `scoped` transaction using the names of the DatabaseProviders you are interested in, and an `async` function (scope) in which to use the connections. Scoped transactions only exist for the scope of the `async` function and are automatically cleaned up at the end of the function. It is best to view `Module.db.scoped()` as an identical mechanism to `@transaction`, behaving in exactly the same way, with a slightly different API:
856
857*modules/database-initializer.js*
858```js
859const Module = require('ravel').Module;
860const prelisten = Module.prelisten;
861
862class DatabaseInitializer extends Module {
863
864 @prelisten // trigger db init on application startup
865 doDbInit (ctx) {
866 const self = this;
867 // specify one or more providers to open connections to, or none
868 // to open connections to all known DatabaseProviders.
869 this.db.scoped('mysql', async function (ctx) {
870 // this async function behaves like koa middleware,
871 // so feel free to await on promises!
872 await self.createTables(ctx.transaction.mysql);
873 await self.insertRows(ctx.transaction.mysql);
874 // notice that this.transaction is identical to ctx.transaction
875 // from @transaction! It's just a hash of open, named connections
876 // to the DatabaseProviders specified.
877 }).catch((err) => {
878 self.log.error(err.stack);
879 process.exit(1); // in this case, we might want to kill our app if db init fails!
880 });
881 }
882
883 /**
884 * @returns {Promise}
885 */
886 createTables (mysqlConnection) { /* ... */ }
887
888 /**
889 * @returns {Promise}
890 */
891 insertRows (mysqlConnection) { /* ... */ }
892}
893
894module.exports = DatabaseInitializer;
895```
896
897### Authentication Providers
898> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/auth/authentication_provider.js.html)
899
900An `AuthenticationProvider` is a lightweight wrapper for a [Passport](https://github.com/jaredhanson/passport) provider library (such as [passport-github](https://github.com/jaredhanson/passport-github)) which performs all the complex set-up and configuration of the library automatically, and registers simple parameters which you must `app.set` (such as OAuth client ids and secrets). The purpose of `AuthenticationProvider`s is to reduce boilerplate code between applications, and simplify often complex `Passport` configuration code. You may use as many different `AuthenticationProvider`s as you wish in your application. Here's an example pulled from [`ravel-github-oauth2-provider`](https://github.com/raveljs/ravel-github-oauth2-provider):
901
902#### Example Setup
903
904*app.js*
905```javascript
906const app = new require('ravel')();
907const GitHubProvider = require('ravel-github-oauth2-provider');
908new GitHubProvider(app);
909// ... other providers and parameters
910(async () => {
911 await app.init();
912});
913// ... the rest of your Ravel app
914```
915
916#### Example Configuration
917
918*.ravelrc.json*
919```json
920{
921 "github auth callback url" : "http://localhost:8080",
922 "github auth path": "/auth/github",
923 "github auth callback path": "/auth/github/callback",
924 "github client id": "YOUR_CLIENT_ID",
925 "github client secret" : "YOUR_CLIENT_SECRET"
926}
927```
928
929You'll also need to implement an `@authconfig` module like this:
930
931*modules/authconfig.js*
932```js
933'use strict';
934
935const Ravel = require('ravel');
936const inject = Ravel.inject;
937const Module = Ravel.Module;
938const authconfig = Module.authconfig;
939
940@authconfig
941@inject('user-profiles')
942class AuthConfig extends Module {
943 constructor (userProfiles) {
944 this.userProfiles = userProfiles;
945 }
946 serializeUser (profile) {
947 // serialize profile to session using the id field
948 return Promise.resolve(profile.id);
949 }
950 deserializeUser (id) {
951 // retrieve profile from database using id from session
952 return this.userProfiles.getProfile(id); // a Promise
953 }
954 verify (providerName, ...args) {
955 // this method is roughly equivalent to the Passport verify callback, but
956 // supports multiple simultaneous AuthenticationProviders.
957 // providerName is the name of the provider which needs credentials verified
958 // args is an array containing credentials, such as username/password for
959 // verification against your database, or a profile and OAuth tokens. See
960 // specific AuthenticationProvider library READMEs for more information about
961 // how to implement this method.
962 }
963}
964
965module.exports = AuthConfig;
966```
967
968#### List of Ravel `AuthenticationProvider`s
969
970Ravel currently supports several `AuthenticationProvider`s via external libraries.
971
972 - [`ravel-github-oauth2-provider`](https://github.com/raveljs/ravel-github-oauth2-provider)
973 - [`ravel-google-oauth2-provider`](https://github.com/raveljs/ravel-google-oauth2-provider)
974
975> If you've written an `AuthenticationProvider` and would like to see it on this list, contact us or open an issue/PR against this README!
976
977### Authentication
978> [<small>View API docs &#128366;</small>](http://raveljs.github.io/docs/latest/auth/decorators/authenticated.js.html)
979
980Once you've registered an `AuthenticationProvider`, requiring users to have an authenticated session to access a `Routes` or `Resource` endpoint is accomplished via the `@authenticated` decorator, which can be used at the class or method level:
981
982*Note: the @authenticated decorator works the same way on `Routes` and `Resource` classes/methods*
983```js
984const Routes = require('ravel').Routes;
985const mapping = Routes.mapping;
986const authenticated = Routes.authenticated;
987
988@authenticated // protect all endpoints in this Routes class
989class MyRoutes extends Routes {
990 constructor () {
991 super('/');
992 }
993
994 @authenticated({redirect: true}) // protect one endpoint specifically
995 @mapping(Routes.GET, 'app')
996 async handler (ctx) {
997 // will redirect to app.get('login route') if not signed in
998 }
999}
1000```
1001
1002## Deployment and Scaling
1003
1004Ravel is designed for horizontal scaling, and helps you avoid common pitfalls when designing your node.js backend application. In particular:
1005
1006 - Session storage in [Redis](https://github.com/antirez/redis) is currently mandatory, ensuring that you can safely replicate your Ravel app safely
1007 - The internal [koa](http://koajs.com/) application's `app.proxy` flag is set to `true`.
1008 - All Ravel dependencies are strictly locked (i.e. no use of `~` or `^` in `package.json`). This helps foster repeatability between members of your team, as well as between development/testing/production environments. Adherence to semver in the node ecosystem is unfortunately varied at best, so it is recommended that you follow the same practice in your app as well.
1009 - While it is possible to color outside the lines, Ravel provides a framework for developing **stateless** backend applications, where all stateful data is stored in external caches or databases.
1010
1011It is strongly encouraged that you containerize your Ravel app using an [Alpine-based docker container](https://hub.docker.com/r/mhart/alpine-node/), and then explore technologies such as [docker-compose](https://www.docker.com/products/docker-compose) or [kubernetes](http://kubernetes.io/) to appropriately scale out and link to (at least) the [official redis container](https://hub.docker.com/_/redis/). An example project with a reference `docker-compose` environment for Ravel can be found in the [starter project](https://github.com/raveljs/ravel-github-mariadb-starter).
1012
1013Ravel does not explicitly require [hiredis](https://github.com/redis/hiredis-node), but is is highly recommended that you install it alongside Ravel for improved redis performance.
1014
1015If you are looking for a good way to share `.ravelrc.json` configuration between multiple replicas of the same Ravel app, have a look at [ravel-etcd-config](https://github.com/raveljs/ravel-etcd-config) for easy distributed configuration.