Skip to content

danielabar/koa-pluralsight

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Table of Contents generated with DocToc

Introduction to Koa

My notes from this Pluralsight course

Course is using Koa 1.x with generator functions and yield. At time of taking this course, Koa is on 2.x and generator functions are deprecated in favor of async/await.

Intro

Benefits of Koa:

  • minimalistic - framework stays out of your way, relies heavily on middleware to put together features you need for your app
  • no calbacks - uses generators, reads sequentially but still non blocking async
  • created by express developers

First Applications

Trivial simple hello world demo:

take The FirstKoaApps
npm init #fill in some values for demo
npm i koa --save
touch app.js

Note use of generator function for middleware, but newer version of koa uses async/await

// app.js
// Bring in koa framework
const koa = require('koa');
// Create application by invoking koa function
const app = koa();

// Use app created above to define middleware
// Asterisk indicates this function is a generator function
// app.use(function * () {
//   this.body = 'Hello World!';
// });
app.use(ctx => {
  ctx.body = 'Hello World!';
});

// Specify which port to listen on
app.listen(3000);
console.log('The app is listening. Port 3000');

Start app:

node app.js

A more fully featured example - HTTP API. Store user information and then retrieve by id.

App

Startup MongoBD, then run app:

docker pull mongo:4.0.0
docker run -p 27017:27017 --name koa-mongo -d mongo:4.0.0
node app2.js

Understanding Yield and Generators

Intro

Sipmle Example 1.

Generator functions marked by * and do not return. They yield, remember where they left off. eg, function that yields a sequence:

function *allTheEvenIntegers() {
  let i = 0;
  while (true) {
    yield i;
    i += 2;
  }
}

To invoke this function, create an instance, then call next:

var evens = allTheEvenIntegers();
// get next value in sequence
console.log(evens.next());
console.log(evens.next());
console.log(evens.next());
console.log(evens.next());
console.log(evens.next());

To run it:

node example1.js

Output - output of next function is an object containing value property and done boolean indicating if there are more values in the sequence. In this example, done is always false because the sequence continues infinitely.

{ value: 0, done: false }
{ value: 2, done: false }
{ value: 4, done: false }
{ value: 6, done: false }
{ value: 8, done: false }

What Can I Use This For

Example 2

In this example, the generator function yields three different values in turn:

function *differentStuff () {
  yield 21;
  yield {name: 'Marcus', age: 42, kids: ['Albert', 'Jane']};
  yield 'A string with data in it';
}

const f = differentStuff();

console.log(f.next());
console.log(f.next());
console.log(f.next());
console.log(f.next());

Output, note on fourth call when there's nothing left to yield, done is true, this means we've reached end of sequence:

{ value: 21, done: false }
{ value: { name: 'Marcus', age: 42, kids: [ 'Albert', 'Jane' ] }, done: false }
{ value: 'A string with data in it', done: false }
{ value: undefined, done: true }

Basically generator function says to caller, when you want the next value, call me back. i.e. similar functionality to callbacks, but without the nested callback hell.

Koa Example Middleware

const Koa = require('koa');
const app = new Koa();

// Define middleware to include response time in header
async function setResponseTime(ctx, next) {
  console.log('=== setResponseTime middleware starting...');
  const start = new Date();
  await next();
  const ms = new Date() - start;
  ctx.set('X-Response-Time', `ms ${ms}`);
  console.log('=== setResponseTime middleware finished');
}

// Define middleware to log response time
async function consoleLogger(ctx, next) {
  console.log('=== consoleLogger middleware starting...');
  const start = new Date();
  await next();
  const ms = new Date() - start;
  console.log(`${ctx.method} ${ctx.url} took ${ms} ms`);
  console.log('=== consoleLogger middleware finished');
}

// Register middlewares
app.use(setResponseTime);
app.use(consoleLogger);

// When / request is invoked, registered middlewares will run
app.use((ctx) => {
  ctx.body = 'Hello World';
});

app.listen(3000);

When next() is encountered, tells Koa to run next middleware, and will continue execution from that point when next middleware is complete. Final middleware is application itself, which sets body and returns. This sends control back to middlewares. When there are no more middlewares to execute entire response is returned.

Hitting http://localhost:3000 in browser outputs to console:

=== setResponseTime middleware starting...
=== consoleLogger middleware starting...
GET / took 1 ms
=== consoleLogger middleware finished
=== setResponseTime middleware finished

Error Handling

Use try/catch. In Koa, if no try/catch used in custom middleware or app, one is supplied for you that returns Server Error 500.

Koa Concepts

Application object used to configure application.

Context object is encapsulation of Request and Response.

The Application Object

To create Application object:

const Koa = require('koa');
const app = new Koa();
// for testing purposes, also expose app object to other modules:
module.exports = Koa;

One liner: const app = module.exports = require('koa)();

listen function starts up app listening on given port number:

const app = require('koa')();
app.listen(3000);
console.log('The app is running. And listening on port 3000');

use function is how Koa includes and uses middleware.

Koa apps built by composing lots of smaller middleware. Middleware can be very small such as single line function to log incoming request to console:

const app = require('koa')();
app.use(ctx => {
  console.dir(ctx.request);
});

Use .use function multiple times to use multiple middlewares, eg: to make use of a logger:

const app = require('koa')();
const logger = require('koa-logger');

app.use(logger());
app.use(ctx => {
  ctx.body = 'Hello World';
});
app.listen(3000);
console.log('The app is running on port 3000');

Can also write a custom logger that logs before and after request by running code before and after await, using next argument provided by Koa:

const app = require('koa')();

app.use(async (ctx, next) => {
  console.log('Before');
  await next();
  console.log('After');
});

app.use(ctx => {
console.log('In application');
ctx.body = 'I\'ve been logged';
});

app.listen(3000);

Summary of Application Object

const app = require('koa')()

app.use(/* middleware */);

app.listen(4321);

The Request Object

Koa Request object is a wrapper around incoming HTTP request object. Can be retrieved from Context:

app.use(ctx => {
  const r = this.request;
});

Useful properties of request object include:

  • request.header
  • request.method (GET, POST, etc)
  • request.url (url request came from)
  • request.path
  • request.querystring

Any of the above can also be set, eg: reqesut.header = ''.

Useful functions of request object include:

request.is('json') or request.is('html') to determine what kind of content is coming in with the request

Use accepts function to do content negotation, eg if your api accepts and handles different content types:

switch (this.request.accepts('json', 'html', 'text')) {
  case 'json': break;
  case 'html': break;
  case 'text': break;
  default: this.throw(406, 'json, html, or text only');
}

If you need access to raw Node Request Object:

const app = require('koa')();
app.use(ctx => {
  console.log(this.request); // koa wrapper
  console.log(this.request.req); // node request
});
app.listen(3000);

The Response Object

Representation of HTTP response sent back to client. Also a wrapper around raw Node response object.

Access it via Context, just like response:

app.use(this => {
  const r = this.response;
});

Useful properties include:

  • response.body = {name: 'Marcus'}; to set response body.
  • response.status = 418 to set http response status code (Koa automatically will add response text, eg 418 - I'm a teapot). If set an invalid response code, Koa will throw an error.
  • response.type = 'application/json' to set response content type.

Useful functions include: response.set('Location', '/user/123') to set a response header. Can also pass in a json object to set several headers at once:

response.set({
  'Etag': '234',
  'Last-Modified': new Date()
});

Redirect to send 302 Moved Temporarily to caller:

response.redirect('http://some.place.net');

To get raw Node response object:

const app = require('koa')();
app.use(ctx => {
  console.log(this.response); // koa wrapper
  console.log(this.response.res); // node response
});
app.listen(3000);

The Context Object

To avoid using this.request/response. Context object shadows and wraps all useful response/response methods.

const method = ctx.method;
ctx.is('html');
ctx.bdoy = {foo: 'bar'};

Building an HTTP API With Koa

Will build simple CRUD HTTP API, this is Koa's sweet spot. Initial setup:

take UserApi
npm init
touch app.js
touch test.js
npm i --save koa

Add start script in package.json:

"scripts": {
  "start": "nodemon app.js",
  "test": "echo \"Error: no test specified\" && exit 1"
}

Then can start app with npm start.

The First Method - Create New Users

Will use TDD approach, testing entire stack, using mocha and supertest. test.js.

Testing is done in-memory, no need to start server, since app object is exposed by app.js:

See test.js

To run the tests:

./node_modules/mocha/bin/mocha -u bdd -R spec --exit

Can also move above command to scripts section of package.json, then run npm test.

Then write code to make test pass, see app.js.

Will use Monk as Mongo wrapper.

Validation

API should have some validation and reject invalid inputs. To send an error response code:

ctx.throw(400, 'error message');

For example:

app.use(routes.post('/user', addUser))

async function addUser(ctx) {
  const userFromRequest = await parse(ctx);

  if (!userFromRequest.name) {
    ctx.throw(400, 'name required');
  }
  try {
    const insertedUser = await users.insert(userFromRequest);
    ctx.body = insertedUser;
    ctx.set('Location', `/user/${insertedUser._id}`);
    ctx.status = 201;
  } catch (err) {
    console.error(err);
    ctx.throw(500, 'unable to save user');
  }
}

Get One

To verify a particular user by id exists, will first have to create it in the test. Can do this by exporting the users collection from app so it's available to test to do inserts.

// app.js
const users = db.get('users');
module.exports.users = users;

// test.js
const a_user = {...};
const insertedUser = await app.users.insert(a_user);

If a route has a parameter such as id in this case:

app.use(routes.get('/user/:id', getUser));

Then the parameter will be sent to the route handler, getUser in this case:

async function getUser(ctx, id) {
  const user = await users.findOne({_id: id});
  if (!user) {
    ctx.throw(404, `no such user with id ${id}`);
  }
  ctx.body = user;
  ctx.status = 200;
}

A problem with tests so far is they have side effects - inserting users in database. This should be cleaned up:

afterEach(async () => {
  await users.remove();
});

Updating and Deleting

To test update, similar to get by id in that we first need to create a user in database.

Refactoring

Move routes to separate file userRoutes.js to clean up app.js.

Move db setup code to userRoutes.js.

Also move all route handler functions to userRoutes.js

Web site

Koa can serve static files using middleware:

const serve = require('koa-static');
app.use(serve(`${__dirname}/public`));

About

Learning Koa with Pluralsight

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published