├── 404 ├── test.js └── app.js ├── .jshintignore ├── templates ├── views │ └── user.ejs ├── test.js └── app.js ├── .eslintrc.yml ├── .travis.yml ├── blog ├── views │ ├── index.html │ ├── show.html │ ├── new.html │ ├── list.html │ └── layout.html ├── lib │ └── render.js ├── app.js └── test.js ├── hello-world ├── app.js └── test.js ├── stream-view ├── app.js ├── test.js ├── view.js └── README.md ├── .editorconfig ├── upload ├── public │ ├── 404.html │ └── index.html └── app.js ├── vhost ├── apps │ ├── koa.js │ └── array.js ├── app.js └── test.js ├── stream-server-side-events ├── README.md ├── sse.js ├── db.js └── app.js ├── cookies ├── app.js └── test.js ├── body-parsing ├── app.js └── test.js ├── stream-objects ├── test.js ├── app.js └── README.md ├── base-auth ├── app.js └── test.js ├── errors ├── test.js └── app.js ├── stream-file ├── app.js ├── test.js └── README.md ├── compose ├── test.js └── app.js ├── csrf ├── app.js └── test.js ├── conditional-middleware └── app.js ├── .gitignore ├── flash-messages ├── test.js └── app.js ├── package.json ├── multipart ├── app.js └── test.js ├── negotiation ├── test.js └── app.js └── Readme.md /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | */test.js -------------------------------------------------------------------------------- /templates/views/user.ejs: -------------------------------------------------------------------------------- 1 |

<%= user.name.first %> is a <%= user.age %> year old <%= user.species %>.

-------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | extends: koa 4 | rules: 5 | no-var: 0 6 | prefer-arrow-callback: 0 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - stable 3 | - 8 4 | language: node_js 5 | script: 6 | - npm run lint 7 | - npm test 8 | -------------------------------------------------------------------------------- /blog/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Blog 4 | 5 | 6 | {% for post in posts %} 7 | {% endfor %} 8 | 9 | -------------------------------------------------------------------------------- /blog/views/show.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block title %}{{ post.title }}{% endblock %} 4 | 5 | {% block content %} 6 |

{{ post.title }}

7 |

{{ post.body }}

8 | {% endblock %} -------------------------------------------------------------------------------- /hello-world/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const app = module.exports = new Koa(); 3 | 4 | app.use(async function(ctx) { 5 | ctx.body = 'Hello World'; 6 | }); 7 | 8 | if (!module.parent) app.listen(3000); 9 | -------------------------------------------------------------------------------- /stream-view/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | 3 | const View = require('./view'); 4 | 5 | const app = module.exports = new Koa(); 6 | 7 | app.use(async function(ctx) { 8 | ctx.type = 'html'; 9 | ctx.body = new View(ctx); 10 | }); 11 | 12 | if (!module.parent) app.listen(3000); 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /blog/lib/render.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | const views = require('koa-views'); 7 | const path = require('path'); 8 | 9 | // setup views mapping .html 10 | // to the swig template engine 11 | 12 | module.exports = views(path.join(__dirname, '/../views'), { 13 | map: { html: 'swig' } 14 | }); 15 | -------------------------------------------------------------------------------- /upload/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Not Found 4 | 10 | 11 | 12 |

Sorry! Can't find that.

13 |

The page you requested cannot be found.

14 | 15 | -------------------------------------------------------------------------------- /vhost/apps/koa.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | 3 | // koa app 4 | 5 | const app = new Koa(); 6 | 7 | app.use(async function(ctx, next) { 8 | await next(); 9 | ctx.set('X-Custom', 'Dub Dub Dub App'); 10 | }); 11 | 12 | app.use(async function(ctx, next) { 13 | await next(); 14 | if ('/' != ctx.url) return; 15 | ctx.body = 'Hello from www app'; 16 | }); 17 | 18 | module.exports = app; 19 | -------------------------------------------------------------------------------- /hello-world/test.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | const server = app.listen(); 3 | const request = require('supertest').agent(server); 4 | 5 | describe('Hello World', function() { 6 | after(function() { 7 | server.close(); 8 | }); 9 | 10 | it('should say "Hello World"', function(done) { 11 | request 12 | .get('/') 13 | .expect(200) 14 | .expect('Hello World', done); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /stream-server-side-events/README.md: -------------------------------------------------------------------------------- 1 | # stream-server-side-events 2 | 3 | This program continually sends events to the web browser. It simulates a series of log file entries that have been appended to a file. 4 | 5 | To invoke, the following command begins listening on localhost:3000. 6 | 7 | node --harmony app.js 8 | 9 | To see results: 10 | 11 | http://localhost:3000 12 | 13 | ## Interesting points 14 | 15 | 1. 16 | -------------------------------------------------------------------------------- /cookies/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This example simply sets the number of views from the same client 3 | * both as a cookie and as a response string. 4 | */ 5 | 6 | const Koa = require('koa'); 7 | const app = module.exports = new Koa(); 8 | 9 | app.use(async function(ctx) { 10 | const n = ~~ctx.cookies.get('view') + 1; 11 | ctx.cookies.set('view', n); 12 | ctx.body = n + ' views'; 13 | }); 14 | 15 | if (!module.parent) app.listen(3000); 16 | -------------------------------------------------------------------------------- /blog/views/new.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block title %}New Post{% endblock %} 4 | 5 | {% block content %} 6 |

New Post

7 |

Create a new post.

8 |
9 |

10 |

11 |

12 |
13 | {% endblock %} -------------------------------------------------------------------------------- /404/test.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | const server = app.listen(); 3 | const request = require('supertest').agent(server); 4 | 5 | describe('404', function() { 6 | after(function() { 7 | server.close(); 8 | }); 9 | 10 | describe('when GET /', function() { 11 | it('should return the 404 page', function(done) { 12 | request 13 | .get('/') 14 | .expect(404) 15 | .expect(/Page Not Found/, done); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /blog/views/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block title %}Posts{% endblock %} 4 | 5 | {% block content %} 6 |

Posts

7 |

You have {{ posts.length }} posts!

8 |

Create a Post

9 | 17 | {% endblock %} -------------------------------------------------------------------------------- /templates/test.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | const server = app.listen(); 3 | const request = require('supertest').agent(server); 4 | 5 | describe('Templates', function() { 6 | after(function() { 7 | server.close(); 8 | }); 9 | 10 | describe('GET /', function() { 11 | it('should respond with a rendered view', function(done) { 12 | request 13 | .get('/') 14 | .expect(200) 15 | .expect('

Tobi is a 3 year old ferret.

', done); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /upload/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Upload 5 | 11 | 12 | 13 |

File Upload

14 |

Try uploading multiple files at a time.

15 |
16 | 17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /vhost/apps/array.js: -------------------------------------------------------------------------------- 1 | // rather than koa apps we can also use array 2 | // bundles of middleware to the same effect. 3 | 4 | async function responseTime(ctx, next) { 5 | const start = new Date(); 6 | await next(); 7 | const ms = new Date() - start; 8 | ctx.set('X-Response-Time', ms + 'ms'); 9 | } 10 | 11 | async function index(ctx, next) { 12 | await next(); 13 | if ('/' != ctx.url) return; 14 | ctx.body = 'Howzit? From bar middleware bundle'; 15 | } 16 | 17 | module.exports = [ 18 | responseTime, 19 | index 20 | ]; 21 | -------------------------------------------------------------------------------- /body-parsing/app.js: -------------------------------------------------------------------------------- 1 | 2 | const Koa = require('koa'); 3 | const koaBody = require('koa-body'); 4 | 5 | const app = module.exports = new Koa(); 6 | 7 | app.use(koaBody({ 8 | jsonLimit: '1kb' 9 | })); 10 | 11 | // POST .name to /uppercase 12 | // co-body accepts application/json 13 | // and application/x-www-form-urlencoded 14 | 15 | app.use(async function(ctx) { 16 | const body = ctx.request.body; 17 | if (!body.name) ctx.throw(400, '.name required'); 18 | ctx.body = { name: body.name.toUpperCase() }; 19 | }); 20 | 21 | if (!module.parent) app.listen(3000); 22 | -------------------------------------------------------------------------------- /stream-objects/test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | const app = require('./app'); 3 | const server = app.listen(); 4 | const request = require('supertest').agent(server); 5 | 6 | describe('Stream Objects', function() { 7 | after(function() { 8 | server.close(); 9 | }); 10 | 11 | it('GET /', function(done) { 12 | request 13 | .get('/app.js') 14 | .expect(200, function(err, res) { 15 | if (err) return done(err); 16 | 17 | res.body.should.eql([{ 18 | id: 1 19 | }, { 20 | id: 2 21 | }]); 22 | done(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /stream-view/test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | const app = require('./app'); 3 | const server = app.listen(); 4 | const request = require('supertest').agent(server); 5 | 6 | describe('Stream View', function() { 7 | after(function() { 8 | server.close(); 9 | }); 10 | 11 | it('GET /', function(done) { 12 | request 13 | .get('/') 14 | .expect(200, function(err, res) { 15 | if (err) return done(err); 16 | 17 | res.should.be.html; 18 | res.text.should.include('Hello World'); 19 | res.text.should.include('

Hello World

'); 20 | done(); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /cookies/test.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | const server = app.listen(); 3 | const request = require('supertest').agent(server); 4 | 5 | describe('Cookies Views', function() { 6 | after(function() { 7 | server.close(); 8 | }); 9 | 10 | [1, 2, 3].forEach(function(i) { 11 | describe('on iteration #' + i, function() { 12 | it('should set the views as a cookie and as the body', function(done) { 13 | request 14 | .get('/') 15 | .expect(200) 16 | .expect('Set-Cookie', new RegExp('view=' + i)) 17 | .expect(i + ' views', done); 18 | }); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /stream-server-side-events/sse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a transform stream that converts a stream 3 | * to valid `data: \n\n' events for SSE. 4 | */ 5 | 6 | var Transform = require('stream').Transform; 7 | var inherits = require('util').inherits; 8 | 9 | module.exports = SSE; 10 | 11 | inherits(SSE, Transform); 12 | 13 | function SSE(options) { 14 | if (!(this instanceof SSE)) return new SSE(options); 15 | 16 | options = options || {}; 17 | Transform.call(this, options); 18 | } 19 | 20 | SSE.prototype._transform = function(data, enc, cb) { 21 | this.push('data: ' + data.toString('utf8') + '\n\n'); 22 | cb(); 23 | }; 24 | -------------------------------------------------------------------------------- /templates/app.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const views = require('koa-views'); 3 | const Koa = require('koa'); 4 | const app = module.exports = new Koa(); 5 | 6 | // setup views, appending .ejs 7 | // when no extname is given to render() 8 | 9 | app.use(views(path.join(__dirname, '/views'), { extension: 'ejs' })); 10 | 11 | // dummy data 12 | 13 | const user = { 14 | name: { 15 | first: 'Tobi', 16 | last: 'Holowaychuk' 17 | }, 18 | species: 'ferret', 19 | age: 3 20 | }; 21 | 22 | // render 23 | 24 | app.use(async function(ctx) { 25 | await ctx.render('user', { user }); 26 | }); 27 | 28 | if (!module.parent) app.listen(3000); 29 | -------------------------------------------------------------------------------- /stream-objects/app.js: -------------------------------------------------------------------------------- 1 | 2 | const Koa = require('koa'); 3 | const JSONStream = require('streaming-json-stringify'); 4 | 5 | const app = module.exports = new Koa(); 6 | 7 | app.use(async function(ctx) { 8 | ctx.type = 'json'; 9 | const stream = ctx.body = JSONStream(); 10 | 11 | stream.on('error', ctx.onerror); 12 | 13 | setImmediate(function() { 14 | stream.write({ 15 | id: 1 16 | }); 17 | 18 | setImmediate(function() { 19 | stream.write({ 20 | id: 2 21 | }); 22 | 23 | setImmediate(function() { 24 | stream.end(); 25 | }); 26 | }); 27 | }); 28 | }); 29 | 30 | if (!module.parent) app.listen(3000); 31 | -------------------------------------------------------------------------------- /404/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | 3 | const app = module.exports = new Koa(); 4 | 5 | app.use(async function pageNotFound(ctx) { 6 | // we need to explicitly set 404 here 7 | // so that koa doesn't assign 200 on body= 8 | ctx.status = 404; 9 | 10 | switch (ctx.accepts('html', 'json')) { 11 | case 'html': 12 | ctx.type = 'html'; 13 | ctx.body = '

Page Not Found

'; 14 | break; 15 | case 'json': 16 | ctx.body = { 17 | message: 'Page Not Found' 18 | }; 19 | break; 20 | default: 21 | ctx.type = 'text'; 22 | ctx.body = 'Page Not Found'; 23 | } 24 | }); 25 | 26 | if (!module.parent) app.listen(3000); 27 | -------------------------------------------------------------------------------- /base-auth/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const auth = require('koa-basic-auth'); 3 | 4 | const app = module.exports = new Koa(); 5 | 6 | // custom 401 handling 7 | 8 | app.use(async function(ctx, next) { 9 | try { 10 | await next(); 11 | } catch (err) { 12 | if (err.status === 401) { 13 | ctx.status = 401; 14 | ctx.set('WWW-Authenticate', 'Basic'); 15 | ctx.body = 'cant haz that'; 16 | } else { 17 | throw err; 18 | } 19 | } 20 | }); 21 | 22 | // require auth 23 | 24 | app.use(auth({ name: 'tj', pass: 'tobi' })); 25 | 26 | // secret response 27 | 28 | app.use(async function(ctx) { 29 | ctx.body = 'secret'; 30 | }); 31 | 32 | if (!module.parent) app.listen(3000); 33 | -------------------------------------------------------------------------------- /errors/test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | const app = require('./app'); 3 | const server = app.listen(); 4 | const request = require('supertest').agent(server); 5 | 6 | describe('Errors', function() { 7 | after(function() { 8 | server.close(); 9 | }); 10 | 11 | it('should catch the error', function(done) { 12 | request 13 | .get('/') 14 | .expect(500) 15 | .expect('Content-Type', /text\/html/, done); 16 | }); 17 | 18 | it('should emit the error on app', function(done) { 19 | app.once('error', function(err, ctx) { 20 | err.message.should.equal('boom boom'); 21 | ctx.should.be.ok; 22 | done(); 23 | }); 24 | 25 | request 26 | .get('/') 27 | .end(function() {}); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /stream-server-side-events/db.js: -------------------------------------------------------------------------------- 1 | var Readable = require('stream').Readable; 2 | var inherits = require('util').inherits; 3 | 4 | /** 5 | * Returns a new subscription event event. 6 | * Real APIs would care about the `event`. 7 | */ 8 | 9 | exports.subscribe = function(event, options) { 10 | return Subscription(options); 11 | }; 12 | 13 | /** 14 | * Subscription stream. Just increments the result. 15 | * Never ends! 16 | */ 17 | 18 | inherits(Subscription, Readable); 19 | 20 | function Subscription(options) { 21 | if (!(this instanceof Subscription)) return new Subscription(options); 22 | 23 | options = options || {}; 24 | Readable.call(this, options); 25 | 26 | this.value = 0; 27 | } 28 | 29 | Subscription.prototype._read = function() { 30 | while (this.push(String(this.value++))) {} 31 | }; 32 | -------------------------------------------------------------------------------- /stream-file/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const fs = require('fs'); 3 | const app = module.exports = new Koa(); 4 | const path = require('path'); 5 | const extname = path.extname; 6 | 7 | // try GET /app.js 8 | 9 | app.use(async function(ctx) { 10 | const fpath = path.join(__dirname, ctx.path); 11 | const fstat = await stat(fpath); 12 | 13 | if (fstat.isFile()) { 14 | ctx.type = extname(fpath); 15 | ctx.body = fs.createReadStream(fpath); 16 | } 17 | }); 18 | 19 | if (!module.parent) app.listen(3000); 20 | 21 | /** 22 | * thunkify stat 23 | */ 24 | 25 | function stat(file) { 26 | return new Promise(function(resolve, reject) { 27 | fs.stat(file, function(err, stat) { 28 | if (err) { 29 | reject(err); 30 | } else { 31 | resolve(stat); 32 | } 33 | }); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /compose/test.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | const server = app.listen(); 3 | const request = require('supertest').agent(server); 4 | 5 | describe('Compose', function() { 6 | after(function() { 7 | server.close(); 8 | }); 9 | 10 | describe('when GET /', function() { 11 | it('should say "Hello World"', function(done) { 12 | request 13 | .get('/') 14 | .expect(200) 15 | .expect('Hello World', done); 16 | }); 17 | 18 | it('should set X-Response-Time', function(done) { 19 | request 20 | .get('/') 21 | .expect('X-Response-Time', /ms$/) 22 | .expect(200, done); 23 | }); 24 | }); 25 | 26 | describe('when not GET /', function() { 27 | it('should 404', function(done) { 28 | request 29 | .get('/aklsjdf') 30 | .expect(404, done); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /csrf/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const koaBody = require('koa-body'); 3 | const session = require('koa-session'); 4 | const CSRF = require('koa-csrf'); 5 | const router = require('@koa/router')(); 6 | 7 | const app = module.exports = new Koa(); 8 | 9 | /** 10 | * csrf need session 11 | */ 12 | 13 | app.keys = ['session key', 'csrf example']; 14 | app.use(session(app)); 15 | app.use(koaBody()); 16 | 17 | /** 18 | * maybe a bodyparser 19 | */ 20 | 21 | /** 22 | * csrf middleware 23 | */ 24 | 25 | app.use(new CSRF()); 26 | 27 | /** 28 | * route 29 | */ 30 | 31 | router.get('/token', token) 32 | .post('/post', post); 33 | 34 | app.use(router.routes()); 35 | 36 | async function token(ctx) { 37 | ctx.body = ctx.csrf; 38 | } 39 | 40 | async function post(ctx) { 41 | ctx.body = {ok: true}; 42 | } 43 | 44 | if (!module.parent) app.listen(3000); 45 | -------------------------------------------------------------------------------- /stream-file/test.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | const server = app.listen(); 3 | const request = require('supertest').agent(server); 4 | 5 | describe('Stream File', function() { 6 | after(function() { 7 | server.close(); 8 | }); 9 | 10 | it('GET /app.js', function(done) { 11 | request 12 | .get('/app.js') 13 | .expect('content-type', /application\/javascript/) 14 | .expect(200, done); 15 | }); 16 | 17 | it('GET /test.js', function(done) { 18 | request 19 | .get('/test.js') 20 | .expect('content-type', /application\/javascript/) 21 | .expect(200, done); 22 | }); 23 | 24 | it('GET /alksjdf.js', function(done) { 25 | request 26 | .get('/lajksdf.js') 27 | .expect(404, done); 28 | }); 29 | 30 | it('GET /', function(done) { 31 | request 32 | .get('/') 33 | .expect(404, done); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /stream-view/view.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Readable = require('stream').Readable; 4 | const co = require('co'); 5 | 6 | module.exports = class View extends Readable { 7 | 8 | constructor(context) { 9 | super(); 10 | 11 | // render the view on a different loop 12 | co.call(this, this.render).catch(context.onerror); 13 | } 14 | 15 | _read() {} 16 | 17 | *render() { 18 | // push the immediately 19 | this.push('Hello World'); 20 | 21 | // render the on the next tick 22 | const body = yield done => { 23 | setImmediate(() => done(null, '

Hello World

')); 24 | }; 25 | 26 | this.push('' + body + ''); 27 | 28 | // close the document 29 | this.push(''); 30 | 31 | // end the stream 32 | this.push(null); 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /conditional-middleware/app.js: -------------------------------------------------------------------------------- 1 | const logger = require('koa-logger'); 2 | const Koa = require('koa'); 3 | const app = new Koa(); 4 | 5 | // passing any middleware to this middleware 6 | // will make it conditional, and will not be used 7 | // when an asset is requested, illustrating how 8 | // middleware may "wrap" other middleware. 9 | 10 | function ignoreAssets(mw) { 11 | return async function(ctx, next) { 12 | if (/(\.js|\.css|\.ico)$/.test(ctx.path)) { 13 | await next(); 14 | } else { 15 | // must .call() to explicitly set the receiver 16 | await mw.call(this, ctx, next); 17 | } 18 | }; 19 | } 20 | 21 | // TRY: 22 | // $ curl http://localhost:3000/ 23 | // $ curl http://localhost:3000/style.css 24 | // $ curl http://localhost:3000/some.html 25 | 26 | app.use(ignoreAssets(logger())); 27 | 28 | app.use(async function(ctx) { 29 | ctx.body = 'Hello World'; 30 | }); 31 | 32 | app.listen(3000); 33 | -------------------------------------------------------------------------------- /base-auth/test.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | const server = app.listen(); 3 | const request = require('supertest').agent(server); 4 | 5 | describe('Koa Basic Auth', function() { 6 | after(function() { 7 | server.close(); 8 | }); 9 | 10 | describe('with no credentials', function() { 11 | it('should `throw` 401', function(done) { 12 | request 13 | .get('/') 14 | .expect(401, done); 15 | }); 16 | }); 17 | 18 | describe('with invalid credentials', function() { 19 | it('should `throw` 401', function(done) { 20 | request 21 | .get('/') 22 | .auth('user', 'invalid password') 23 | .expect(401, done); 24 | }); 25 | }); 26 | 27 | describe('with valid credentials', function() { 28 | it('should call the next middleware', function(done) { 29 | request 30 | .get('/') 31 | .auth('tj', 'tobi') 32 | .expect(200) 33 | .expect('secret', done); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /stream-server-side-events/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const app = module.exports = new Koa(); 3 | 4 | const sse = require('./sse'); 5 | const db = require('./db'); 6 | 7 | app.use(async function(ctx) { 8 | // otherwise node will automatically close this connection in 2 minutes 9 | ctx.req.setTimeout(Number.MAX_VALUE); 10 | 11 | ctx.type = 'text/event-stream; charset=utf-8'; 12 | ctx.set('Cache-Control', 'no-cache'); 13 | ctx.set('Connection', 'keep-alive'); 14 | 15 | const body = ctx.body = sse(); 16 | const stream = db.subscribe('some event'); 17 | stream.pipe(body); 18 | 19 | // if the connection closes or errors, 20 | // we stop the SSE. 21 | const socket = ctx.socket; 22 | socket.on('error', close); 23 | socket.on('close', close); 24 | 25 | function close() { 26 | stream.unpipe(body); 27 | socket.removeListener('error', close); 28 | socket.removeListener('close', close); 29 | } 30 | }); 31 | 32 | if (!module.parent) app.listen(3000); 33 | -------------------------------------------------------------------------------- /stream-view/README.md: -------------------------------------------------------------------------------- 1 | # stream-view 2 | 3 | This is a "Hello World" application, using a view that inherits from a Readable stream. 4 | 5 | To invoke, the following command begins listening on localhost:3000. 6 | 7 | node app.js 8 | 9 | To see results: 10 | 11 | http://localhost:3000 12 | 13 | ## Interesting points 14 | 15 | 1. The main function of app.js instantiates a "View" from the view.js file. 16 | 2. The View overrides the Readable's _read() function with an empty function. 17 | 3. The View also overrides the Readable's render() function to do the following: 18 | 1. Immediately push out the text for the \ of the page 19 | 2. Yield to a function that will ultimately (in the next tick) return the "Hello World" text. The render() function pauses at that point. 20 | 3. When that function returns, render() resumes, and assigns the return value to the `body` variable 21 | 4. Push out the returned text wrapped in \ tags 22 | 5. Push out the closing \ tag and 23 | 6. Close the connection with push(null) 24 | -------------------------------------------------------------------------------- /errors/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const app = module.exports = new Koa(); 3 | 4 | // look ma, error propagation! 5 | 6 | app.use(async function(ctx, next) { 7 | try { 8 | await next(); 9 | } catch (err) { 10 | // some errors will have .status 11 | // however this is not a guarantee 12 | ctx.status = err.status || 500; 13 | ctx.type = 'html'; 14 | ctx.body = '

Something exploded, please contact Maru.

'; 15 | 16 | // since we handled this manually we'll 17 | // want to delegate to the regular app 18 | // level error handling as well so that 19 | // centralized still functions correctly. 20 | ctx.app.emit('error', err, ctx); 21 | } 22 | }); 23 | 24 | // response 25 | 26 | app.use(async function() { 27 | throw new Error('boom boom'); 28 | }); 29 | 30 | // error handler 31 | 32 | app.on('error', function(err) { 33 | if (process.env.NODE_ENV != 'test') { 34 | console.log('sent error %s to the cloud', err.message); 35 | console.log(err); 36 | } 37 | }); 38 | 39 | if (!module.parent) app.listen(3000); 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store* 32 | ehthumbs.db 33 | Icon? 34 | Thumbs.db 35 | .idea 36 | 37 | # Node.js # 38 | ########### 39 | lib-cov 40 | *.seed 41 | *.log 42 | *.csv 43 | *.dat 44 | *.out 45 | *.pid 46 | *.gz 47 | 48 | pids 49 | logs 50 | results 51 | 52 | node_modules 53 | npm-debug.log 54 | 55 | # Components # 56 | ############## 57 | 58 | /build 59 | /components 60 | 61 | # ImageMagick # 62 | ############### 63 | 64 | *.cache 65 | *.mpc 66 | 67 | # Other # 68 | ######### 69 | test/*.2 70 | test/*/*.2 71 | test/*.mp4 72 | test/images/originalSideways.jpg.2 73 | 74 | # Traceur output # 75 | ################## 76 | out/* -------------------------------------------------------------------------------- /stream-objects/README.md: -------------------------------------------------------------------------------- 1 | # stream-objects 2 | 3 | Stream a out Javascript objects. To invoke, the following command begins listening on localhost:3000. 4 | 5 | node --harmony app.js 6 | 7 | To see results: 8 | 9 | http://localhost:3000 10 | 11 | ## Interesting points 12 | 13 | 1. In app.js, the setImmediate() function writes out a JS object { id: 1 } to the stream, then declares another setImmediate function. 14 | 2. The second setImmediate() function writes a second JS object { id: 2 } to the stream, then declares a third setImmediate() function. 15 | 3. The final setImmediate() calls stream.end() to indicate that there is no more data. 16 | 4. Note that the setImmediate() calls do **not** happen at the same moment. The first setImmediate() call is executed sometime after the initial request arrived. That setImmediate() call then declares the second setImmediate() call, which happens at least one tick later, and the third setImmediate() call happens in a separate tick. 17 | 4. The resulting web page shows an array containing both the JS objects, in the order they were initiated, like this: 18 | 19 | [ 20 | {"id":1} 21 | , 22 | {"id":2} 23 | ] 24 | -------------------------------------------------------------------------------- /flash-messages/test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | const app = require('./app'); 3 | const server = app.listen(); 4 | const request = require('supertest').agent(server); 5 | 6 | describe('Flash Messages', function() { 7 | after(function() { 8 | server.close(); 9 | }); 10 | 11 | it('GET should return an empty array', function(done) { 12 | request 13 | .get('/messages') 14 | .expect(200) 15 | .expect('content-type', 'application/json; charset=utf-8') 16 | .expect('[]', done); 17 | }); 18 | 19 | it('POST should return 204', function(done) { 20 | request 21 | .post('/messages') 22 | .send('hello') 23 | .expect(204, done); 24 | }); 25 | 26 | it('GET should return the message', function(done) { 27 | request 28 | .get('/messages') 29 | .expect(200) 30 | .expect('content-type', 'application/json; charset=utf-8') 31 | .expect('["hello"]', done); 32 | }); 33 | 34 | it('GET should return no more messages', function(done) { 35 | request 36 | .get('/messages') 37 | .expect(200) 38 | .expect('content-type', 'application/json; charset=utf-8') 39 | .expect('[]', done); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /blog/views/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block title %}Blog{% endblock %} 4 | 54 | 55 | 56 |
57 | {% block content %} 58 |

Missing content!

59 | {% endblock %} 60 |
61 | 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-examples", 3 | "description": "Examples using Koa", 4 | "version": "0.0.1", 5 | "repository": "https://github.com/koajs/examples", 6 | "bugs": "https://github.com/koajs/examples/issues", 7 | "dependencies": { 8 | "ejs": "^2.5.6", 9 | "fs-promise": "^2.0.3", 10 | "koa": "^2.2.0", 11 | "koa-basic-auth": "^2.0.0", 12 | "koa-body": "^4.0.8", 13 | "koa-compose": "^4.0.0", 14 | "koa-csrf": "^3.0.6", 15 | "koa-logger": "^3.0.0", 16 | "@koa/router": "^8.0.5", 17 | "koa-session": "^5.0.0", 18 | "koa-static": "^3.0.0", 19 | "koa-views": "^6.0.2", 20 | "streaming-json-stringify": "^3.1.0", 21 | "swig-templates": "^2.0.3" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^3.8.1", 25 | "eslint-config-koa": "^2.0.2", 26 | "eslint-config-standard": "^6.2.0", 27 | "eslint-plugin-promise": "^3.3.0", 28 | "eslint-plugin-standard": "^2.0.1", 29 | "mocha": "^5.0.0", 30 | "should": "^3.3.2", 31 | "supertest": "^3.0.0" 32 | }, 33 | "scripts": { 34 | "test": "NODE_ENV=test mocha --harmony --reporter spec --require should */test.js", 35 | "lint": "eslint ." 36 | }, 37 | "engines": { 38 | "node": ">= 7.6" 39 | }, 40 | "license": "MIT" 41 | } 42 | -------------------------------------------------------------------------------- /flash-messages/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A very simple flash example. 3 | * Only uses JSON for simplicity. 4 | */ 5 | 6 | const Koa = require('koa'); 7 | const rawBody = require('raw-body'); 8 | const session = require('koa-session'); 9 | 10 | const app = module.exports = new Koa(); 11 | 12 | // required for signed cookie sessions 13 | app.keys = ['key1', 'key2']; 14 | app.use(session(app)); 15 | 16 | app.use(async function(ctx, next) { 17 | if (ctx.method !== 'GET' || ctx.path !== '/messages') return await next(); 18 | 19 | // get any messages saved in the session 20 | const messages = ctx.session.messages || []; 21 | ctx.body = messages; 22 | 23 | // delete the messages as they've been deliverd 24 | delete ctx.session.messages; 25 | }); 26 | 27 | app.use(async function(ctx, next) { 28 | if (ctx.method !== 'POST' || ctx.path !== '/messages') return await next(); 29 | 30 | // the request string is the flash message 31 | const message = await rawBody(ctx.req, { 32 | encoding: 'utf8' 33 | }); 34 | 35 | // push the message to the session 36 | ctx.session.messages = ctx.session.messages || []; 37 | ctx.session.messages.push(message); 38 | 39 | // tell the client everything went okay 40 | ctx.status = 204; 41 | }); 42 | 43 | if (!module.parent) app.listen(3000); 44 | -------------------------------------------------------------------------------- /upload/app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | const logger = require('koa-logger'); 7 | const serve = require('koa-static'); 8 | const koaBody = require('koa-body'); 9 | const Koa = require('koa'); 10 | const fs = require('fs'); 11 | const app = new Koa(); 12 | const os = require('os'); 13 | const path = require('path'); 14 | 15 | // log requests 16 | 17 | app.use(logger()); 18 | 19 | app.use(koaBody({ multipart: true })); 20 | 21 | // custom 404 22 | 23 | app.use(async function(ctx, next) { 24 | await next(); 25 | if (ctx.body || !ctx.idempotent) return; 26 | ctx.redirect('/404.html'); 27 | }); 28 | 29 | // serve files from ./public 30 | 31 | app.use(serve(path.join(__dirname, '/public'))); 32 | 33 | // handle uploads 34 | 35 | app.use(async function(ctx, next) { 36 | // ignore non-POSTs 37 | if ('POST' != ctx.method) return await next(); 38 | 39 | const file = ctx.request.files.file; 40 | const reader = fs.createReadStream(file.path); 41 | const stream = fs.createWriteStream(path.join(os.tmpdir(), Math.random().toString())); 42 | reader.pipe(stream); 43 | console.log('uploading %s -> %s', file.name, stream.path); 44 | 45 | ctx.redirect('/'); 46 | }); 47 | 48 | // listen 49 | 50 | app.listen(3000); 51 | console.log('listening on port 3000'); 52 | -------------------------------------------------------------------------------- /multipart/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Multipart example downloading all the files to disk using co-busboy. 3 | * If all you want is to download the files to a temporary folder, 4 | * just use https://github.com/cojs/multipart instead of copying this code 5 | * as it handles file descriptor limits whereas this does not. 6 | */ 7 | 8 | const os = require('os'); 9 | const path = require('path'); 10 | const Koa = require('koa'); 11 | const fs = require('fs-promise'); 12 | const koaBody = require('koa-body'); 13 | 14 | const app = module.exports = new Koa(); 15 | 16 | app.use(koaBody({ multipart: true })); 17 | 18 | app.use(async function(ctx) { 19 | // create a temporary folder to store files 20 | const tmpdir = path.join(os.tmpdir(), uid()); 21 | 22 | // make the temporary directory 23 | await fs.mkdir(tmpdir); 24 | const filePaths = []; 25 | const files = ctx.request.files || {}; 26 | 27 | for (let key in files) { 28 | const file = files[key]; 29 | const filePath = path.join(tmpdir, file.name); 30 | const reader = fs.createReadStream(file.path); 31 | const writer = fs.createWriteStream(filePath); 32 | reader.pipe(writer); 33 | filePaths.push(filePath); 34 | } 35 | 36 | ctx.body = filePaths; 37 | }); 38 | 39 | if (!module.parent) app.listen(3000); 40 | 41 | function uid() { 42 | return Math.random().toString(36).slice(2); 43 | } 44 | -------------------------------------------------------------------------------- /body-parsing/test.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | const server = app.listen(); 3 | const request = require('supertest').agent(server); 4 | 5 | describe('Body Parsing', function() { 6 | after(function() { 7 | server.close(); 8 | }); 9 | 10 | describe('POST /uppercase', function() { 11 | describe('with JSON', function() { 12 | it('should work', function(done) { 13 | request 14 | .post('/uppercase') 15 | .send({ name: 'tobi' }) 16 | .expect(200) 17 | .expect({ name: 'TOBI' }, done); 18 | }); 19 | }); 20 | 21 | describe('with urlencoded', function() { 22 | it('should work', function(done) { 23 | request 24 | .post('/uppercase') 25 | .send('name=tj') 26 | .expect(200) 27 | .expect({ name: 'TJ' }, done); 28 | }); 29 | }); 30 | 31 | describe('when length > limit', function() { 32 | it('should 413', function(done) { 33 | request 34 | .post('/json') 35 | .send({ name: Array(5000).join('a') }) 36 | .expect(413, done); 37 | }); 38 | }); 39 | 40 | describe('when no name is sent', function() { 41 | it('should 400', function(done) { 42 | request 43 | .post('/uppsercase') 44 | .send('age=10') 45 | .expect(400, done); 46 | }); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /compose/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Each `app.use()` only accepts a single generator function. 3 | * If you want to combine multiple generator functions into a single one, 4 | * you can use `koa-compose` to do so. 5 | * This allows you to use `app.use()` only once. 6 | * Your code will end up looking something like: 7 | * 8 | * app.use(compose([ 9 | * function *(){}, 10 | * function *(){}, 11 | * function *(){} 12 | * ])) 13 | */ 14 | 15 | const compose = require('koa-compose'); 16 | const Koa = require('koa'); 17 | const app = module.exports = new Koa(); 18 | 19 | // x-response-time 20 | 21 | async function responseTime(ctx, next) { 22 | const start = new Date(); 23 | await next(); 24 | const ms = new Date() - start; 25 | ctx.set('X-Response-Time', ms + 'ms'); 26 | } 27 | 28 | // logger 29 | 30 | async function logger(ctx, next) { 31 | const start = new Date(); 32 | await next(); 33 | const ms = new Date() - start; 34 | if ('test' != process.env.NODE_ENV) { 35 | console.log('%s %s - %s', ctx.method, ctx.url, ms); 36 | } 37 | } 38 | 39 | // response 40 | 41 | async function respond(ctx, next) { 42 | await next(); 43 | if ('/' != ctx.url) return; 44 | ctx.body = 'Hello World'; 45 | } 46 | 47 | // composed middleware 48 | 49 | const all = compose([ 50 | responseTime, 51 | logger, 52 | respond 53 | ]); 54 | 55 | app.use(all); 56 | 57 | if (!module.parent) app.listen(3000); 58 | -------------------------------------------------------------------------------- /blog/app.js: -------------------------------------------------------------------------------- 1 | 2 | const render = require('./lib/render'); 3 | const logger = require('koa-logger'); 4 | const router = require('@koa/router')(); 5 | const koaBody = require('koa-body'); 6 | 7 | const Koa = require('koa'); 8 | const app = module.exports = new Koa(); 9 | 10 | // "database" 11 | 12 | const posts = []; 13 | 14 | // middleware 15 | 16 | app.use(logger()); 17 | 18 | app.use(render); 19 | 20 | app.use(koaBody()); 21 | 22 | // route definitions 23 | 24 | router.get('/', list) 25 | .get('/post/new', add) 26 | .get('/post/:id', show) 27 | .post('/post', create); 28 | 29 | app.use(router.routes()); 30 | 31 | /** 32 | * Post listing. 33 | */ 34 | 35 | async function list(ctx) { 36 | await ctx.render('list', { posts: posts }); 37 | } 38 | 39 | /** 40 | * Show creation form. 41 | */ 42 | 43 | async function add(ctx) { 44 | await ctx.render('new'); 45 | } 46 | 47 | /** 48 | * Show post :id. 49 | */ 50 | 51 | async function show(ctx) { 52 | const id = ctx.params.id; 53 | const post = posts[id]; 54 | if (!post) ctx.throw(404, 'invalid post id'); 55 | await ctx.render('show', { post: post }); 56 | } 57 | 58 | /** 59 | * Create a post. 60 | */ 61 | 62 | async function create(ctx) { 63 | const post = ctx.request.body; 64 | const id = posts.push(post) - 1; 65 | post.created_at = new Date(); 66 | post.id = id; 67 | ctx.redirect('/'); 68 | } 69 | 70 | // listen 71 | 72 | if (!module.parent) app.listen(3000); 73 | -------------------------------------------------------------------------------- /stream-file/README.md: -------------------------------------------------------------------------------- 1 | # stream-file 2 | 3 | Stream a file from the local directory. To invoke, the following command begins listening on localhost:3000. 4 | 5 | node --harmony app.js 6 | 7 | To see results: 8 | 9 | http://localhost:3000/README.md or 10 | http://localhost:3000/other-file-in-the-directory 11 | 12 | ## Interesting points 13 | 14 | 1. The stat() function at the bottom of app.js returns another function that will call the normal fs.stat() to get information about the named file. (The function stat() is a promise - it will ultimately return a value, although it may take a while.) 15 | 2. When any program *yields* to a function, it pauses while that function proceeds asynchronously, and eventually returns a value. When the function returns, the program resumes at that point. 16 | 3. In the example, app.use() starts everything off with `fstat = yield stat(fpath)`. We say it "yields to the stat() function." That is, app.use() pauses while the stat() function begins to execute (asynchronously), and the node interpreter goes off to work on other tasks. When the fs.stat() call completes and returns a value, app.use() resumes, and sets the value of `fstat` to the value returned by stat(). 17 | 4. This example also uses the createReadStream() function to create a stream which is another way to handle data (asynchronously) from a file. 18 | 5. `this.body` gets the result of the fs.createReadStream() (which is the stream's data) and sends it to the web browser client that has connected in to the URL above. 19 | -------------------------------------------------------------------------------- /csrf/test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | const app = require('./app'); 3 | const server = app.listen(); 4 | const request = require('supertest').agent(server); 5 | 6 | let token; 7 | let cookie; 8 | 9 | describe('csrf', function() { 10 | after(function() { 11 | server.close(); 12 | }); 13 | 14 | describe('GET /token', function() { 15 | it('should get token', function(done) { 16 | request 17 | .get('/token') 18 | .expect(200) 19 | .end(function(err, res) { 20 | token = res.text; 21 | token.should.be.String; 22 | cookie = res.headers['set-cookie'].join(';'); 23 | done(err); 24 | }); 25 | }); 26 | }); 27 | 28 | describe('POST /post', function() { 29 | it('should 403 without token', function(done) { 30 | request 31 | .post('/post') 32 | .send({foo: 'bar'}) 33 | .expect(403, done); 34 | }); 35 | 36 | it('should 403 with wrong token', function(done) { 37 | request 38 | .post('/post') 39 | .send({foo: 'bar'}) 40 | .set('x-csrf-token', 'wrong token') 41 | .expect(403, done); 42 | }); 43 | 44 | it('should 200 with token in head', function(done) { 45 | request 46 | .post('/post') 47 | .set('Cookie', cookie) 48 | .set('x-csrf-token', token) 49 | .send({foo: 'bar'}) 50 | .expect(200, done); 51 | }); 52 | 53 | it('should 200 with token in body', function(done) { 54 | request 55 | .post('/post') 56 | .set('Cookie', cookie) 57 | .send({_csrf: token}) 58 | .expect(200, done); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /vhost/app.js: -------------------------------------------------------------------------------- 1 | const compose = require('koa-compose'); 2 | const Koa = require('koa'); 3 | const app = module.exports = new Koa(); 4 | 5 | // virtual host apps 6 | 7 | const wwwSubdomain = composer(require('./apps/koa')); 8 | const barSubdomain = composer(require('./apps/array')); 9 | 10 | // compose koa apps and middleware arrays 11 | // to be used later in our host switch generator 12 | 13 | function composer(app) { 14 | const middleware = app instanceof Koa ? app.middleware : app; 15 | return compose(middleware); 16 | } 17 | 18 | // look ma, global response logging for all our apps! 19 | 20 | app.use(async function(ctx, next) { 21 | const start = new Date(); 22 | await next(); 23 | const ms = new Date() - start; 24 | if ('test' != process.env.NODE_ENV) { 25 | console.log('%s %s %s - %sms', ctx.host, ctx.method, ctx.url, ms); 26 | } 27 | }); 28 | 29 | // switch between appropriate hosts calling their 30 | // composed middleware with the appropriate context. 31 | 32 | app.use(async function(ctx, next) { 33 | switch (ctx.hostname) { 34 | case 'example.com': 35 | case 'www.example.com': 36 | // displays `Hello from main app` 37 | // and sets a `X-Custom` response header 38 | return await wwwSubdomain.apply(this, [ctx, next]); 39 | case 'bar.example.com': 40 | // displays `Howzit? From bar middleware bundle` 41 | // and sets a `X-Response-Time` response header 42 | return await barSubdomain.apply(this, [ctx, next]); 43 | } 44 | 45 | // everything else, eg: 127.0.0.1:3000 46 | // will propagate to 404 Not Found 47 | return await next(); 48 | }); 49 | 50 | if (!module.parent) app.listen(3000); 51 | -------------------------------------------------------------------------------- /blog/test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | const app = require('./app'); 3 | const server = app.listen(); 4 | const request = require('supertest').agent(server); 5 | 6 | describe('Blog', function() { 7 | after(function() { 8 | server.close(); 9 | }); 10 | 11 | describe('GET /', function() { 12 | it('should see title "Posts"', function(done) { 13 | request 14 | .get('/') 15 | .expect(200, function(err, res) { 16 | if (err) return done(err); 17 | res.should.be.html; 18 | res.text.should.include('Posts'); 19 | done(); 20 | }); 21 | }); 22 | 23 | it('should see 0 post', function(done) { 24 | request 25 | .get('/') 26 | .expect(200, function(err, res) { 27 | if (err) return done(err); 28 | 29 | res.should.be.html; 30 | res.text.should.include('

You have 0 posts!

'); 31 | done(); 32 | }); 33 | }); 34 | }); 35 | 36 | describe('POST /post/new', function() { 37 | it('should create post and redirect to /', function(done) { 38 | request 39 | .post('/post') 40 | .send({title: 'Title', body: 'Contents'}) 41 | .end(function(err, res) { 42 | if (err) return done(err); 43 | 44 | res.header.location.should.be.equal('/'); 45 | done(); 46 | }); 47 | }); 48 | }); 49 | 50 | describe('GET /post/0', function() { 51 | it('should see post', function(done) { 52 | request 53 | .get('/post/0') 54 | .expect(200, function(err, res) { 55 | if (err) return done(err); 56 | 57 | res.should.be.html; 58 | res.text.should.include('

Title

'); 59 | res.text.should.include('

Contents

'); 60 | done(); 61 | }); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /negotiation/test.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | const server = app.listen(); 3 | const request = require('supertest').agent(server); 4 | 5 | describe('negotiation', function() { 6 | after(function() { 7 | server.close(); 8 | }); 9 | 10 | describe('json', function() { 11 | it('should respond with json', function(done) { 12 | request 13 | .get('/tobi') 14 | .set('Accept', 'application/json') 15 | .expect(200) 16 | .expect('Content-Type', /json/) 17 | .expect({ name: 'tobi', species: 'ferret' }, done); 18 | }); 19 | }); 20 | 21 | describe('xml', function() { 22 | it('should respond with xml', function(done) { 23 | request 24 | .get('/tobi') 25 | .set('Accept', 'application/xml') 26 | .expect(200) 27 | .expect('Content-Type', /xml/) 28 | .expect('tobi', done); 29 | }); 30 | }); 31 | 32 | describe('html', function() { 33 | it('should respond with html', function(done) { 34 | request 35 | .get('/tobi') 36 | .set('Accept', 'text/html') 37 | .expect(200) 38 | .expect('Content-Type', /html/) 39 | .expect('

tobi

', done); 40 | }); 41 | }); 42 | 43 | describe('text', function() { 44 | it('should respond with html', function(done) { 45 | request 46 | .get('/tobi') 47 | .set('Accept', 'text/plain') 48 | .expect(200) 49 | .expect('Content-Type', /plain/) 50 | .expect('tobi', done); 51 | }); 52 | }); 53 | 54 | describe('*/*', function() { 55 | it('should give precedence to the first accepted type', function(done) { 56 | request 57 | .get('/tobi') 58 | .set('Accept', '*/*') 59 | .expect(200) 60 | .expect('Content-Type', /json/) 61 | .expect('{"name":"tobi","species":"ferret"}', done); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /negotiation/app.js: -------------------------------------------------------------------------------- 1 | 2 | const Koa = require('koa'); 3 | const app = module.exports = new Koa(); 4 | 5 | const tobi = { 6 | _id: '123', 7 | name: 'tobi', 8 | species: 'ferret' 9 | }; 10 | 11 | const loki = { 12 | _id: '321', 13 | name: 'loki', 14 | species: 'ferret' 15 | }; 16 | 17 | const users = { 18 | tobi: tobi, 19 | loki: loki 20 | }; 21 | 22 | // content negotiation middleware. 23 | // note that you should always check for 24 | // presence of a body, and sometimes you 25 | // may want to check the type, as it may 26 | // be a stream, buffer, string, etc. 27 | 28 | app.use(async function(ctx, next) { 29 | await next(); 30 | 31 | // no body? nothing to format, early return 32 | if (!ctx.body) return; 33 | 34 | // Check which type is best match by giving 35 | // a list of acceptable types to `req.accepts()`. 36 | const type = ctx.accepts('json', 'html', 'xml', 'text'); 37 | 38 | // not acceptable 39 | if (type === false) ctx.throw(406); 40 | 41 | // accepts json, koa handles this for us, 42 | // so just return 43 | if (type === 'json') return; 44 | 45 | // accepts xml 46 | if (type === 'xml') { 47 | ctx.type = 'xml'; 48 | ctx.body = '' + ctx.body.name + ''; 49 | return; 50 | } 51 | 52 | // accepts html 53 | if (type === 'html') { 54 | ctx.type = 'html'; 55 | ctx.body = '

' + ctx.body.name + '

'; 56 | return; 57 | } 58 | 59 | // default to text 60 | ctx.type = 'text'; 61 | ctx.body = ctx.body.name; 62 | }); 63 | 64 | // filter responses, in this case remove ._id 65 | // since it's private 66 | 67 | app.use(async function(ctx, next) { 68 | await next(); 69 | 70 | if (!ctx.body) return; 71 | 72 | delete ctx.body._id; 73 | }); 74 | 75 | // try $ GET /tobi 76 | // try $ GET /loki 77 | 78 | app.use(async function(ctx) { 79 | const name = ctx.path.slice(1); 80 | const user = users[name]; 81 | ctx.body = user; 82 | }); 83 | 84 | if (!module.parent) app.listen(3000); 85 | -------------------------------------------------------------------------------- /vhost/test.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | const server = app.listen(); 3 | const request = require('supertest').agent(server); 4 | 5 | describe('Virtual Host', function() { 6 | after(function() { 7 | server.close(); 8 | }); 9 | 10 | describe('www subdomain koa app', function() { 11 | describe('when GET /', function() { 12 | it('should say "Hello from www app"', function(done) { 13 | request 14 | .get('/') 15 | .set('Host', 'www.example.com') 16 | .expect(200) 17 | .expect('Hello from www app', done); 18 | }); 19 | 20 | it('should set X-Custom', function(done) { 21 | request 22 | .get('/') 23 | .set('Host', 'www.example.com') 24 | .expect('X-Custom', 'Dub Dub Dub App') 25 | .expect(200, done); 26 | }); 27 | }); 28 | 29 | describe('when GET / without subdomain', function() { 30 | it('should say "Hello from www app"', function(done) { 31 | request 32 | .get('/') 33 | .set('Host', 'example.com') 34 | .expect(200) 35 | .expect('Hello from www app', done); 36 | }); 37 | 38 | it('should set X-Custom', function(done) { 39 | request 40 | .get('/') 41 | .set('Host', 'example.com') 42 | .expect('X-Custom', 'Dub Dub Dub App') 43 | .expect(200, done); 44 | }); 45 | }); 46 | 47 | describe('when not GET /', function() { 48 | it('should 404', function(done) { 49 | request 50 | .get('/aklsjdf') 51 | .set('Host', 'example.com') 52 | .expect(404, done); 53 | }); 54 | }); 55 | }); 56 | describe('bar subdomain array bundle', function() { 57 | describe('when GET /', function() { 58 | it('should say "Howzit? From bar middleware bundle"', function(done) { 59 | request 60 | .get('/') 61 | .set('Host', 'bar.example.com') 62 | .expect(200) 63 | .expect('Howzit? From bar middleware bundle', done); 64 | }); 65 | 66 | it('should set X-Response-Time', function(done) { 67 | request 68 | .get('/') 69 | .set('Host', 'bar.example.com') 70 | .expect('X-Response-Time', /ms$/) 71 | .expect(200, done); 72 | }); 73 | }); 74 | 75 | describe('when not GET /', function() { 76 | it('should 404', function(done) { 77 | request 78 | .get('/aklsjdf') 79 | .set('Host', 'bar.example.com') 80 | .expect(404, done); 81 | }); 82 | }); 83 | }); 84 | describe('default vhost', function() { 85 | describe('when GET /', function() { 86 | it('should 404', function(done) { 87 | request 88 | .get('/') 89 | .set('Host', '127.0.0.1') 90 | .expect(404, done); 91 | }); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /multipart/test.js: -------------------------------------------------------------------------------- 1 | require('should'); 2 | const fs = require('fs'); 3 | const app = require('./app'); 4 | const server = app.listen(); 5 | const request = require('supertest').agent(server); 6 | 7 | // https://github.com/mscdex/busboy/blob/master/test/test-types-multipart.js 8 | const ct = 'multipart/form-data; boundary=---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k'; 9 | const body = [ 10 | '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', 11 | 'Content-Disposition: form-data; name="file_name_0"', 12 | '', 13 | 'super alpha file', 14 | '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', 15 | 'Content-Disposition: form-data; name="file_name_1"', 16 | '', 17 | 'super beta file', 18 | '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', 19 | 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"', 20 | 'Content-Type: application/octet-stream', 21 | '', 22 | 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', 23 | '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', 24 | 'Content-Disposition: form-data; name="upload_file_1"; filename="1k_b.dat"', 25 | 'Content-Type: application/octet-stream', 26 | '', 27 | 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', 28 | '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' 29 | ].join('\r\n'); 30 | 31 | describe('Multipart Files', function() { 32 | after(function() { 33 | server.close(); 34 | }); 35 | 36 | it('should store all the files', function(done) { 37 | request 38 | .post('/') 39 | .set('Content-Type', ct) 40 | .send(body) 41 | .expect(200) 42 | .end(function(err, res) { 43 | if (err) return done(err); 44 | 45 | const files = res.body; 46 | files.should.have.length(2); 47 | fs.stat(files[0], function(err) { 48 | if (err) return done(err); 49 | 50 | fs.stat(files[1], done); 51 | }); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Koa Examples 2 | 3 | A repository containing small examples to illustrate the use of Koa 4 | for creating web applications and other HTTP servers. 5 | 6 | # Running tests 7 | 8 | ```bash 9 | npm test 10 | npm run lint 11 | ``` 12 | 13 | ## Included Examples 14 | 15 | - [404](404) - 404 handling 16 | - [base-auth](base-auth) - middleware base auth example 17 | - [blog](blog) - multi-route & view rendering 18 | - [body-parsing](body-parsing) - request body parsing 19 | - [compose](compose) - compose middlewares example 20 | - [conditional-middleware](conditional-middleware) - shows how middleware may be conditionally applied 21 | - [cookies](cookies) - cookie usage example 22 | - [csrf](csrf) - middleware csrf example 23 | - [errors](errors) - error handling & propagation 24 | - [flash-messages](flash-messages) - flash example 25 | - [hello-world](hello-world) - hello world application 26 | - [multipart](multipart) - multipart example downloading files using co-busboy 27 | - [negotiation](negotiation) - negotiation usage example 28 | - [stream-file](stream-file) - simple file streaming 29 | - [stream-objects](stream-objects) - objects streaming 30 | - [stream-server-side-events](stream-server-side-events) - server side events streaming 31 | - [stream-view](stream-view) - view streaming 32 | - [templates](templates) - simple view rendering 33 | - [upload](upload) - multi-file uploading 34 | - [vhost](vhost) - virtual host example 35 | 36 | ## Example Repositories 37 | 38 | - [coko](https://github.com/bhaskarmelkani/coko) - A minimal convention over configuration framework/boilerplate for Koa 2. 39 | - [kails](https://github.com/embbnux/kails) - A Web App like Rails build with Koa v2, Webpack and Postgres 40 | - [muffin](https://github.com/muffinjs/server) - A content management system build on top of Koa v2 41 | - [links](https://github.com/juliangruber/links) - experimental content sharing and collaboration platform 42 | - [component-crawler](https://github.com/component/crawler.js) - crawl users and organizations for repositories with `component.json`s 43 | - [bigpipe](https://github.com/jonathanong/bigpipe-example) - Facebook's BigPipe implementation in koa and component 44 | - [webcam-mjpeg-stream](https://github.com/jonathanong/webcam-mjpeg-stream) - stream JPEG snapshots from your Mac 45 | - [cnpmjs.org](https://github.com/cnpm/cnpmjs.org) - Private npm registry and web for Enterprise, base on koa, MySQL and Simple Store Service 46 | - [blog-mongo](https://github.com/marcusoftnet/koablog-mongo) - the blog example from this repo, but using a MongoDb database, and tests 47 | - [koa-rest](https://github.com/hemanth/koa-rest) - A simple app to demo REST API 48 | - [koajs-rest-skeleton](https://github.com/ria-com/node-koajs-rest-skeleton) - A simple Koa REST Skeleton Application 49 | - [koa-bookshelf](https://github.com/Tomsqualm/koa-bookshelf) - Koa example with CRUD, using MongoDB and Heroku comptability 50 | - [todo](https://github.com/koajs/todo) - A todo example written in koa and [react](http://facebook.github.io/react/) 51 | - [koa-skeleton](https://github.com/danneu/koa-skeleton) - A simple made-to-be-forked Koa app that uses Postgres and deploys to Heroku. 52 | - Live demo: 53 | - [nodejs-docs-samples](https://github.com/GoogleCloudPlatform/nodejs-docs-samples/tree/master/appengine/koa) - An example Koa app and tutorial for deploying to Google App Engine 54 | - Live demo: 55 | - [koa-passport-mongoose-graphql](https://github.com/sibeliusseraphini/koa-passport-mongoose-graphql) - Koa 2 starterkit using mongoose, graphql setup, and authentication with passport 56 | - [hacknical](https://github.com/ecmadao/hacknical) - A website for github user to make a better resume, based on Koa v2, redis and mongoose. 57 | - [koa-vue-notes-api](https://github.com/johndatserakis/koa-vue-notes-api) - A fleshed-out SPA using Koa 2.3 on the backend and Vue 2.4 on the frontend. Includes fully featured user-authentication components, CRUD actions for the user's notes, and async/await. 58 | - [koa-typescript-node](https://github.com/Talento90/typescript-node) - Template for building nodejs and typescript services. Features: MySql, Migrations, Docker, Unit & Integration Tests, JWT authentication, authorization, graceful shutdown, Prettier. 59 | - [koa-shell](https://github.com/lifeeka/koa-shell) - Structured sample skeleton application for microservices and api development with Koa. 60 | 61 | ## Boilerplates 62 | 63 | - [koa2-boilerplate](https://github.com/geekplux/koa2-boilerplate) - A minimal boilerplate of koa v2 development 64 | - [api-boilerplate](https://github.com/koajs/api-boilerplate) - API application boilerplate 65 | - [component-koa-et-al-boilerplate](https://github.com/sunewt/component-koa-et-al-boilerplate) - Server/client boilerplate with component, livereload, and more 66 | - [koa-typescript-starter](https://github.com/ddimaria/koa-typescript-starter) - A Koa2 starter kit using TypeScript, ES6 imports/exports, Travis, Coveralls, Jasmine, Chai, Istanbul/NYC, Lodash, Nodemon, Docker, & Swagger 67 | 68 | ## Yeoman Generators 69 | - [koa-rest](https://github.com/PatrickWolleb/generator-koa-rest) - RESTful API scaffolder with subgenerator 70 | - [koa](https://github.com/peter-vilja/generator-koa) - Web Application scaffolder 71 | - [k](https://github.com/minghe/generator-k) - Web Application scaffolder with Chinese README 72 | 73 | ## Articles 74 | 75 | - [Building a RESTful API with Koa and Postgres](http://mherman.org/blog/2017/08/23/building-a-restful-api-with-koa-and-postgres) 76 | - [User Authentication with Passport and Koa](http://mherman.org/blog/2018/01/02/user-authentication-with-passport-and-koa) 77 | --------------------------------------------------------------------------------