
This is the continuation of the article
Writing microservice on KoaJS 2 in the style of ES2017. 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'm lazy but responsive.
Koa is minimalist, even the router is not included in the framework itself. Now we quickly complete it with modules that require a minimum voltage from the programmer.
Formulation of the problem
A banal example: There is a
table of goods (products) on 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 . In the root folder of the service there will be only one file that connects babel and our application from the folder ./app
require('babel-core/register'); const app = require('./app');
Settings for babel rendered in
.babelrcThe 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.
Our custom middlewares will be created in the ./middleware folder.
In order to give information about errors in JSON-format, we will write ./middleware/error.js
export default async (ctx, next) => { try { await next(); } catch (err) {
Routes could be placed in the main file, but then the code would seem more complicated.
The backbone of the service is ready.
We write routes
For routs there are many modules, including those with support for koa 2, I prefer
koa-router , we consider the strengths of this module in 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 the comment from rumkin , I redid the last line with export - it was laconic, but not on Feng Shui. Well and, accordingly, corrected import in ./app/index.js
We connect the
product model, which we will talk about below, as well as the
koa-body module, which parses the body of the post-request into the object. With
koa-convert we convert
koa-body into 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 to the address
/ product , we get all the records from the model and transfer them to ctx.body for transferring to JSON to the client. All the necessary headers koa install itself.
But the POST request is processed more interestingly - in the route, starting from 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 body of the request, before this data will be processed by the following middleware.
On routes all, if you have any more questions, ask them in the comments.
What errors and in what cases returns REST anywhere is not described unambiguously, "it is possible so, it is possible that way". I code it in the way it would be convenient for me, so if you have any comments on this part, then I will consider them with pleasure.
Create a model "product"
As a database, I chose MySQL, 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 cannot say that this is a masterpiece of engineering misley, since it is rigidly tied 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 the switch function because applied advanced features for the transfer of parameters from the standard ES6 / ES2015. In this way, as in the example, we can organize the transfer of parameters without worrying about the order in which they are followed; in addition, we can set “default values” for unspecified parameters.
We are 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, which will help to make the code more simple and understandable.
Instead of conclusion
I understand that few of the admirers express, restify, and others. Well-established frameworks are ready to drop everything and code on a framework whose core of fans is only being formed, and which changes quite radically from version to version. A little middleware has been written to koa 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 Habré.
In the comments to the first part of the article, the
user fawer wrote that async / await is already available in chrome 52 without babel . The future is near, don't miss it!
useful links