├── .gitignore ├── Makefile ├── Readme.md ├── api ├── stats │ ├── config.json │ ├── index.js │ └── test.js └── users │ ├── config.json │ ├── index.js │ └── test.js ├── bin └── api ├── index.js ├── lib └── load │ └── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @NODE_ENV=test ./node_modules/.bin/mocha \ 4 | --require should \ 5 | --reporter spec \ 6 | --harmony \ 7 | --bail \ 8 | api/*/test.js 9 | 10 | .PHONY: test -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Koa API Boilerplate 3 | 4 | Boilerplate API application structure - at least one flavour. 5 | 6 | ## Installation 7 | 8 | cloneeeee 9 | 10 | ## Usage 11 | 12 | ``` 13 | 14 | Usage: api [options] 15 | 16 | Options: 17 | 18 | -h, --help output usage information 19 | -H, --host specify the host [0.0.0.0] 20 | -p, --port specify the port [4000] 21 | -b, --backlog specify the backlog size [511] 22 | 23 | ``` 24 | 25 | ## Structure 26 | 27 | Resources and associated tests are defined in ./api 28 | 29 | ## Tests 30 | 31 | Run `make test` 32 | 33 | ## API Versioning 34 | 35 | Use a proxy for `/v1`, `/v2` etc and launch new `api(1)` programs, don't version 36 | in the same application, it's brittle, bloaty and pointless. 37 | 38 | # License 39 | 40 | MIT -------------------------------------------------------------------------------- /api/stats/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stats", 3 | "description": "api statistics", 4 | "routes": { 5 | "GET /stats": "all", 6 | "GET /stats/:name": "get" 7 | } 8 | } -------------------------------------------------------------------------------- /api/stats/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * This file illustrates how you may map 4 | * single routes using config.json instead 5 | * of resource-based routing. 6 | */ 7 | 8 | var stats = { 9 | requests: 100000, 10 | average_duration: 52, 11 | uptime: 123123132 12 | }; 13 | 14 | /** 15 | * GET all stats. 16 | */ 17 | 18 | exports.all = function *(){ 19 | this.body = stats; 20 | }; 21 | 22 | /** 23 | * GET a single stat. 24 | */ 25 | 26 | exports.get = function *(){ 27 | this.body = stats[this.params.name]; 28 | }; 29 | -------------------------------------------------------------------------------- /api/stats/test.js: -------------------------------------------------------------------------------- 1 | 2 | var request = require('supertest'); 3 | var api = require('../..'); 4 | 5 | describe('GET /stats', function(){ 6 | it('should respond with stats', function(done){ 7 | var app = api(); 8 | 9 | request(app.listen()) 10 | .get('/stats') 11 | .expect({ 12 | requests: 100000, 13 | average_duration: 52, 14 | uptime: 123123132 15 | }) 16 | .end(done); 17 | }) 18 | }) 19 | 20 | describe('GET /stats/:name', function(){ 21 | it('should respond with a single stat', function(done){ 22 | var app = api(); 23 | 24 | request(app.listen()) 25 | .get('/stats/requests') 26 | .expect('100000', done); 27 | }) 28 | }) -------------------------------------------------------------------------------- /api/users/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "users", 3 | "description": "user account resource" 4 | } -------------------------------------------------------------------------------- /api/users/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var parse = require('co-body'); 7 | 8 | /** 9 | * This file illustrates using resourceful 10 | * routing using the koa-router module. 11 | */ 12 | 13 | var users = { 14 | tobi: { 15 | name: 'tobi', 16 | age: 3, 17 | species: 'ferret' 18 | }, 19 | 20 | loki: { 21 | name: 'loki', 22 | age: 2, 23 | species: 'ferret' 24 | }, 25 | 26 | jane: { 27 | name: 'jane', 28 | age: 7, 29 | species: 'ferret' 30 | } 31 | }; 32 | 33 | /** 34 | * GET all users. 35 | */ 36 | 37 | exports.index = function *(){ 38 | this.body = users; 39 | }; 40 | 41 | /** 42 | * GET user by :name. 43 | */ 44 | 45 | exports.show = function *(){ 46 | this.body = users[this.params.user]; 47 | }; 48 | 49 | /** 50 | * POST a new user. 51 | */ 52 | 53 | exports.create = function *(name){ 54 | var body = yield parse(this); 55 | if (!body.name) this.throw(400, '.name required'); 56 | users[body.name] = body; 57 | this.status = 201; 58 | this.body = 'added!'; 59 | }; 60 | 61 | -------------------------------------------------------------------------------- /api/users/test.js: -------------------------------------------------------------------------------- 1 | 2 | var request = require('supertest'); 3 | var api = require('../..'); 4 | 5 | describe('GET /users', function(){ 6 | it('should respond with users', function(done){ 7 | var app = api(); 8 | 9 | request(app.listen()) 10 | .get('/users') 11 | .end(function(err, res){ 12 | if (err) return done(err); 13 | Object.keys(res.body).should.eql(['tobi', 'loki', 'jane']); 14 | done(); 15 | }); 16 | }) 17 | it('should respond with users/:id', function(done){ 18 | var app = api(); 19 | 20 | request(app.listen()) 21 | .get('/users/jane') 22 | .end(function(err, res){ 23 | if (err) return done(err); 24 | Object.keys(res.body).should.eql(['name', 'age', 'species']); 25 | done(); 26 | }); 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /bin/api: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var program = require('commander'); 8 | var api = require('..'); 9 | 10 | // options 11 | 12 | program 13 | .option('-H, --host ', 'specify the host [0.0.0.0]', '0.0.0.0') 14 | .option('-p, --port ', 'specify the port [4000]', '4000') 15 | .option('-b, --backlog ', 'specify the backlog size [511]', '511') 16 | .option('-r, --ratelimit ', 'ratelimit requests [2500]', '2500') 17 | .option('-d, --ratelimit-duration ', 'ratelimit duration [1h]', '1h') 18 | .parse(process.argv); 19 | 20 | // create app 21 | 22 | var app = api({ 23 | ratelimit: ~~program.ratelimit, 24 | duration: ~~program.ratelimitDuration 25 | }); 26 | 27 | // listen 28 | 29 | app.listen(program.port, program.host, ~~program.backlog); 30 | console.log('Listening on %s:%s', program.host, program.port); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var responseTime = require('koa-response-time'); 7 | var ratelimit = require('koa-ratelimit'); 8 | var compress = require('koa-compress'); 9 | var logger = require('koa-logger'); 10 | var router = require('koa-router'); 11 | var load = require('./lib/load'); 12 | var redis = require('redis'); 13 | var koa = require('koa'); 14 | 15 | /** 16 | * Environment. 17 | */ 18 | 19 | var env = process.env.NODE_ENV || 'development'; 20 | 21 | /** 22 | * Expose `api()`. 23 | */ 24 | 25 | module.exports = api; 26 | 27 | /** 28 | * Initialize an app with the given `opts`. 29 | * 30 | * @param {Object} opts 31 | * @return {Application} 32 | * @api public 33 | */ 34 | 35 | function api(opts) { 36 | opts = opts || {}; 37 | var app = koa(); 38 | 39 | // logging 40 | 41 | if ('test' != env) app.use(logger()); 42 | 43 | // x-response-time 44 | 45 | app.use(responseTime()); 46 | 47 | // compression 48 | 49 | app.use(compress()); 50 | 51 | // rate limiting 52 | 53 | app.use(ratelimit({ 54 | max: opts.ratelimit, 55 | duration: opts.duration, 56 | db: redis.createClient() 57 | })); 58 | 59 | // routing 60 | 61 | app.use(router(app)); 62 | 63 | // boot 64 | 65 | load(app, __dirname + '/api'); 66 | 67 | return app; 68 | } 69 | -------------------------------------------------------------------------------- /lib/load/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Resource = require('koa-resource-router'); 7 | var debug = require('debug')('api'); 8 | var path = require('path'); 9 | var fs = require('fs'); 10 | var join = path.resolve; 11 | var readdir = fs.readdirSync; 12 | 13 | /** 14 | * Load resources in `root` directory. 15 | * 16 | * TODO: move api.json (change name?) 17 | * bootstrapping into an npm module. 18 | * 19 | * TODO: adding .resources to config is lame, 20 | * but assuming no routes is also lame, change 21 | * me 22 | * 23 | * @param {Application} app 24 | * @param {String} root 25 | * @api private 26 | */ 27 | 28 | module.exports = function(app, root){ 29 | readdir(root).forEach(function(file){ 30 | var dir = join(root, file); 31 | var stats = fs.lstatSync(dir); 32 | if (stats.isDirectory()) { 33 | var conf = require(dir + '/config.json'); 34 | conf.name = file; 35 | conf.directory = dir; 36 | if (conf.routes) route(app, conf); 37 | else resource(app, conf); 38 | } 39 | }); 40 | }; 41 | 42 | /** 43 | * Define routes in `conf`. 44 | */ 45 | 46 | function route(app, conf) { 47 | debug('routes: %s', conf.name); 48 | 49 | var mod = require(conf.directory); 50 | 51 | for (var key in conf.routes) { 52 | var prop = conf.routes[key]; 53 | var method = key.split(' ')[0]; 54 | var path = key.split(' ')[1]; 55 | debug('%s %s -> .%s', method, path, prop); 56 | 57 | var fn = mod[prop]; 58 | if (!fn) throw new Error(conf.name + ': exports.' + prop + ' is not defined'); 59 | 60 | app[method.toLowerCase()](path, fn); 61 | } 62 | } 63 | 64 | /** 65 | * Define resource in `conf`. 66 | */ 67 | 68 | function resource(app, conf) { 69 | if (!conf.name) throw new Error('.name in ' + conf.directory + '/config.json is required'); 70 | debug('resource: %s', conf.name); 71 | 72 | var mod = require(conf.directory); 73 | app.use(Resource(conf.name, mod).middleware()); 74 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-api-boilerplate", 3 | "version": "0.0.1", 4 | "repository": "koajs/api-boilerplate", 5 | "description": "Koa API boilerplate application", 6 | "keywords": [ 7 | "koa", 8 | "boilerplate", 9 | "api" 10 | ], 11 | "dependencies": { 12 | "koa-response-time": "~1.0.2", 13 | "koa-ratelimit": "~1.0.3", 14 | "koa-compress": "1.0.7", 15 | "koa-logger": "~1.2.1", 16 | "koa": "~0.6.1", 17 | "commander": "~2.2.0", 18 | "debug": "*", 19 | "koa-router": "~3.1.4", 20 | "koa-resource-router": "^0.3.3", 21 | "co-body": "0.0.1", 22 | "redis": "~0.10.3" 23 | }, 24 | "devDependencies": { 25 | "mocha": "*", 26 | "should": "*", 27 | "supertest": "~0.8.2" 28 | }, 29 | "license": "MIT" 30 | } 31 | --------------------------------------------------------------------------------