We are writing a microservice on KoaJS 2 in ES2017 style. Part II: Minimalistic REST

    Koa v2

    This is a continuation of the article. We are writing a microservice on KoaJS 2 in ES2017 style. Part I: Such different asynchrony . I will try to please the novice developer who wants to part with express, but does not know how. There will be a lot of code, not enough text - I am lazy but responsive.

    Koa is minimalistic, even the router is not included in the framework itself. Now we will quickly equip it with such modules that require a minimum voltage from the programmer.

    Formulation of the problem


    Banal example: There is a table of products (products) in MySQL. Our task is to give the opportunity to add / delete / change products in this table through the REST service that we have to write.

    Let's start


    Create 2 folders ./config and ./app . There will be only one file in the root folder of the service that connects babel and our application from the ./app folder
    require('babel-core/register');
    const app = require('./app');
    

    Settings for babel made in .babelrc

    The main file of our application will look like this:
    import Koa from 'koa';
    import config from  'config';
    import err from './middleware/error';
    import {routes, allowedMethods} from './middleware/routes';
    const app = new Koa();
    app.use(err);
    app.use(routes());
    app.use(allowedMethods());
    app.listen(config.server.port, function () {
        console.log('%s listening at port %d', config.app.name, config.server.port);
    });
    


    For storing configuration settings, I recommend the config module . It allows you to conveniently organize the configuration, up to a separate instance.

    We will create our custom middleware in the ./middleware folder.

    In order to give information about errors in JSON format, write ./middleware/error.js
    export default async (ctx, next) => {
        try {
            await next();
        } catch (err) {
            // will only respond with JSON
            ctx.status = err.statusCode || err.status || 500;
            ctx.body = {
                message: err.message
            };
        }
    }
    


    Routes could be placed in the main file, but then the code will seem more complicated.

    The backbone of the service is ready.

    Writing Routes


    There are many modules for routes, including those with koa 2 support, I prefer koa-router , we will consider the strengths of this module using our example:
    import Router from 'koa-router';
    import product from '../models/product';
    import convert from 'koa-convert';
    import KoaBody from 'koa-body';
    const router = new Router(),
          koaBody = convert(KoaBody());
        router
            .get('/product', async (ctx, next) => {
                ctx.body = await product.getAll()
            })
            .get('/product/:id', async (ctx, next) => {
                let result = await product.get(ctx.params.id);
                if (result) {
                    ctx.body = result
                } else {
                    ctx.status = 204
                }
            })
            .post('/product', koaBody, async (ctx, next) => {
                ctx.status = 201;
                ctx.body = await product.create(ctx.request.body)
            })
            .put('/product/:id', koaBody, async (ctx, next) => {
                ctx.status = 204;
                await product.update(ctx.params.id, ctx.request.body);
            })
            .delete('/product/:id', async (ctx, next) => {
                ctx.status = 204;
                await product.delete(ctx.params.id);
            });
    export function routes () { return router.routes() }
    export function allowedMethods () { return router.allowedMethods() }
    


    UPD: According to a remark from rumkin , I redid the last line with the export - it was concise, but not feng shui. Well and, accordingly, import corrected in ./app/index.js

    We connect the product model , which we will talk about a little later, as well as the koa-body module , which parses the body of the post-request to the object. Using koa-convert, we will convert koa-body to middleware for koa 2.

    In the simplest case, the route looks predictable:
            .get('/product', async (ctx, next) => {
                ctx.body = await product.getAll()
            })
    

    In the case of a get request at the / product address , we get all the records from the model and pass them to ctx.body for transfer to the JSON client. All necessary headers koa installs itself.

    But the POST request is processed more interestingly - in the route, starting with the second argument, you can add an unlimited number of middleware. In our case, this gives us the opportunity to connect koa-body to get the request body before this data is processed by the next middleware.

    Everything by routes, if you still have questions, ask them in the comments.

    What errors and in what cases REST returns are nowhere unequivocally described, "it is possible so, it is possible that way." I coded as it would be convenient for me, so if you have comments on this part, I will consider them with pleasure.

    Create the “product” model


    I chose MySQL as the database, simply because I had it “at hand”.
    import query from 'mysql-query-promise';
    import config from  'config';
    const tableName = config.product.tableName;
    const crud = {
        getAll: async () => {
            return query(`SELECT * from ${tableName}`);
        },
        get: async (id) => {
            let products = await query(`SELECT * FROM ${tableName} WHERE id=?`,[Number(id)]);
            return products[0];
        },
        create: async function ({ id, name, price = 0, currency = 'UAH' }) {
            let product = {name: String(name), price: Number(price), currency: String(currency)};
            if (id > 0) product.id = Number(id);
            let result = await query(`INSERT INTO ${tableName} SET ? ON DUPLICATE KEY UPDATE ?`,[product,product]);
            if (result.insertId) id = result.insertId;
            return crud.get(id);
        },
        update: async (id, product)=> {
            if (typeof product === 'object') {
                let uProduct = {};
                if (product.hasOwnProperty('name')) uProduct.name = String(product.name);
                if (product.hasOwnProperty('price')) uProduct.price = Number(product.price);
                if (product.hasOwnProperty('currency')) uProduct.currency = String(product.currency);
                let result = await query(`UPDATE ${tableName} SET ? WHERE id=?`,[uProduct, Number(id)]);
                return result.affectedRows;
            }
        },
        delete: async (id) => {
            let result = await query(`DELETE FROM ${tableName} WHERE id=?`,[Number(id)]);
            return result.affectedRows;
        }
    };
    export default crud;
    

    The mysql-query-promise module is written in our company, I can’t say that this is a masterpiece of engineering, since it is rigidly attached to the config module. But in our case it is applicable.
    Simple methods get, delete, put, it makes no sense to comment, there the code speaks for itself, but I will tell you a little about POST. I did not use arrow function since applied advanced capabilities for passing parameters from the ES6 / ES2015 standard. In the same way as in the example, we can organize the transfer of parameters without worrying about the order of their sequence, in addition, you can set “default values” for undefined parameters.

    Testing


    Everything, our service is ready, you can start it and test it from the command line:
    curl -XPOST "127.0.0.1:3001/product" -d '{"id":1,"name":"Test","price":1}' -H 'Content-Type: application/json'
    curl -XGET "127.0.0.1:3001/product"
    curl -XPUT "127.0.0.1:3001/product/1" -d '{"name":"Test1"}' -H 'Content-Type: application/json'
    curl -XGET "127.0.0.1:3001/product/1"
    curl -XDELETE "127.0.0.1:3001/product/1"
    

    Everything works, I will be glad to your suggestions that will help make the code even more simple and understandable.

    Instead of a conclusion


    I understand that few fans of express, restify, and other established frameworks are ready to drop everything and code on a framework in which the core of admirers is just being formed, and which is changing quite radically from version to version. By koa, little middleware has been written so far and there is practically no documentation in Russian. But despite this, I made my choice 2 years ago, when I learned about Koa from a random comment on a hub

    In the comments to the first part of the article , fawer wrote that async / await is already available in chrome 52 without babel . The future is near, do not miss it!

    useful links



    Also popular now: