Building Rest APIs with koa-jsDanilo Assis
Danilo Assis
@daniloab
@daniloab_
Fullstack Developer

API

  • Application Programming Interface
  • Rest Architecture
  • Resources
  • Versioning
  • Operations and HTTP Verbs
  • KoaJS

  • About
  • Cascading
  • Router Module
  • Auth
  • Context
  • Error Handling
  • Testing your API

    • Jest
    • Fixtures
    • Supertest
    • MongoDB

    API - what is

  • Application Programming Interface
  • Systems integration
  • Data security
  • Information exchange
  • Bridges
  • Example

    Common APIs

    • Facebook and Gmail login
    • NASA
    • Google Maps
    • Pokemon

    Rest Architecture

  • Abstraction
  • Resource Identifier
  • Media Type to define the data type (JSON, XML)
  • HTTP Verbs/Methods (GET, POST, PUT, DELETE)
  • Stateless
  • Cacheable
  • Resources

  • Abstraction
  • Resource Identifier
  • Resource representations shall be self-descriptive
  • Resources Examples

    • /users
    • /companies
    • /areas

    Resources Examples - how don't nominate

    • /getUsers
    • /companies/udpate
    • /areas/delete

    Versioning

    Keep the control of your apis and show only major versions
  • https://api/users/v1/user
  • https://api/users/v2/user
  • Operations

    Keep the control of your apis and show only major versions
    image: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods

    Operations

    Keep the control of your apis and show only major versions
    image: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods

    Operations Examples

    https://www.restapitutorial.com/lessons/httpmethods.html
    koa
    next generation web framework for node.js
    • builded by team behind express
    • focused on be smaller, more expressive, and more robust foundation for web applications and APIs
    • made for web developers build fast and scalable network applications

    Small footprint - Hello World

    // KoaHelloWorld
    const Koa = require('koa');
    const app = new Koa();
    app.use(async ctx => {
    ctx.body = 'Hello World';
    });
    app.listen(3000);

    The obligatory hello world application

    Cascading

    • avoid the use of callbacks
    • friendly use of async functions
    • downstream and upstream

    Cascading - Example

    const Koa = require('koa');
    const app = new Koa();
    // logger
    app.use(async (ctx, next) => {
    await next();
    const rt = ctx.response.get('X-Response-Time');
    console.log(`${ctx.method} ${ctx.url} - ${rt}`);
    });
    // x-response-time
    app.use(async (ctx, next) => {
    const start = Date.now();
    await next();
    const ms = Date.now() - start;
    ctx.set('X-Response-Time', `${ms}ms`);
    });
    // response
    app.use(async ctx => {
    ctx.body = 'Hello World';
    });
    app.listen(3000);

    Router Module

    • Express-style routing using app.get, app.put, app.post, etc.
    • Named URL parameters.
    • Named routes with URL generation.
    • Responds to OPTIONS requests with allowed methods.
    • Support for 405 Method Not Allowed and 501 Not Implemented.
    • Multiple route middleware.
    • Multiple routers.
    • Nestable routers.
    • ES7 async/await support.
    • https://github.com/ZijianHe/koa-router

    Auth

    • used with koa router
    • middleware

    Auth - Example

    import { User } from './models'
    improt { getToken } from './getToken';
    export async function AuthFunction (ctx, token) {
    if (!token) return { user: null }
    ctx.status = 401;
    ctx.body = {
    status: 'ERROR',
    message: 'Unauthorized',
    };
    try {
    // function to decode token
    const { user: userId } = getToken(token);
    const user = await User.findOne({ _id: userId.id })
    if (user) {
    ctx.status = 401;
    ctx.body = {
    status: 'ERROR',
    message: 'Unauthorized',
    };
    return;
    }
    ctx.user = user;
    await next();
    } catch (err) {
    return { user: null }
    }
    }

     

    Auth - Example

    // AppAuthFunction
    const Koa = require('koa');
    const app = new Koa();
    import Router from 'koa-router';
    import { auth } from './auth';
    const routerAuth = new Router();
    const routerOpen = new Router();
    //Open APIS (APIs that dont need to Authenticate)
    routerOpen.get('/api/version', ctx => {
    ctx.status = 200;
    ctx.body = {
    status: OK,
    message: version,
    };
    });
    app.use(routerOpen.routes());
    //Authorized APIs
    //Beyond this points APIS need to be Authenticated
    routerAuth.use(auth);
    // auth
    routerAuth.post('/api/auth/v1/login/email', authEmail);
    routerAuth.post('/api/auth/v1/login/password', authPassword);
    app.use(routerAuth.routes());
    // Default not found 404
    app.use(ctx => {
    ctx.status = 404;
    });

     

    Context

    A Context is created per request, and is referenced in middleware as the receiver, or the ctx identifier, as shown in the following snippet:

    app.use(async ctx => {
      ctx; // is the Context
      ctx.request; // is a Koa Request
      ctx.response; // is a Koa Response
    });
    

    Context

    • Unified object
    • Methods and accessors
    • Encapsulate request and response object
    • Set value to be used as like

    Error Handling

    In Express, you caught errors by adding an middleware with a (err, req, res, next) signature as one of the last middleware. In contrast, in koa you add a middleware that does try catch as one of the first middleware. It is also recommended that you emit an error on the application itself for centralized error reporting, retaining the default behaviour in Koa.

    Error Handling - Example

    app.use(async (ctx, next) => {
    try {
    await next();
    } catch (err) {
    ctx.status = err.status || 500;
    ctx.body = err.message;
    ctx.app.emit('error', err, ctx);
    }
    });
    app.on('error', (err, ctx) => {
    /* centralized error handling:
    * console.log error
    * write error to log file
    * save error and request information to database if ctx.request match condition
    * ...
    */
    });

     

    Testing your API

    Supertest

    • High and low level of abstration
    • Simple to use/implement
    • https://github.com/visionmedia/supertest

    Supertest Example

    it('should get api version correctly', async () => {
    const response = await createGetApiCall({ url });
    expect(response.body).toMatchSnapshot();
    expect(response.status).toBe(200);
    });

    Supertest Calls Example

    type ApiArgs = {
    url: string | null;
    authorization: string | null;
    payload: {} | null;
    };
    export const createApiCall = async (args: ApiArgs) => {
    const { url, authorization, payload: body } = args;
    const payload = {
    ...body,
    };
    const response = await request(app.callback())
    .post(url)
    .set({
    Accept: 'application/json',
    'Content-Type': 'application/json',
    ...(authorization ? { authorization } : {}),
    })
    .send(JSON.stringify(payload));
    return response;
    };
    export const createGetApiCall = async (args: ApiArgs) => {
    const { url, authorization } = args;
    const response = await request(app.callback())
    .get(url)
    .set({
    Accept: 'application/json',
    'Content-Type': 'application/json',
    ...(authorization ? { authorization } : {}),
    })
    .send();
    return response;
    };
    export const createDeleteApiCall = async (args: ApiArgs) => {
    const { url, authorization } = args;
    const response = await request(app.callback())
    .delete(url)
    .set({
    Accept: 'application/json',
    'Content-Type': 'application/json',
    ...(authorization ? { authorization } : {}),
    })
    .send();
    return response;
    };

     

    Fixture

    • Avoid repeated issues
    • make easier your life or for who will test your api
    • save time
    • production
    • infinity
    • works perfectly with monorepo

    Create Tenant Fixture

    export const createTenant = async (args: DeepPartial<ITenant> = {}) => {
    const { name, domainName } = args;
    const n = (global.__COUNTERS__.company += 1);
    const company = await new TenantModel({
    name: name ?? `Awesome Tenant ${n}`,
    domainName: domainName ?? 'test.application.com',
    active: true,
    }).save();
    return company;
    };

    Create User Fixture

    export const createUser = async (args: CreateUserArgs = {}): Promise<IUser> => {
    let { email, tenant, password, ...restArgs } = args;
    const n = (global.__COUNTERS__.user += 1);
    if (!email) {
    email = `user${n}@example.com`;
    }
    if (!tenant) {
    tenant = await getOrCreate(TenantModel, createTenant);
    }
    return new UserModel({
    name: args.name || `Normal user ${n}`,
    password: password || '123456',
    email,
    tenant,
    ...restArgs,
    }).save();
    };

    Get or Create Model Fixture

    export const getOrCreate = async (model: Model<any>, createFn: () => any) => {
    const data = await model.findOne().lean();
    if (data) {
    return data;
    }
    return createFn();
    };

    Simple Get User Test

    it('should return user by object id', async () => {
    const tenant = await createTenant();
    const user = await createUser({ tenant });
    // my token uses tenant and user
    const authorization = base64(`${tenant._id}:${user._id}`);
    const url = getUrl(user._id.toString());
    const response = await createGetApiCall({ url, authorization });
    expect(response.status).toBe(200);
    expect(response.body.user).not.toBe(null);
    });

    Learn in public

    Monorepo to find all of these thigs learned today!

    • https://github.com/daniloab/fullstack-playground
    • Issues to contribute with open source
    • Playground

    Thanks!
    We are hiring!
    Join Us

    Give me a Feedback:
    https://entria.feedback.house/danilo