Some developers don’t like react because it’s to hard, functional, new thinking. In this tutorial I’ll show you how easy and fast to create production ready full stack app with node.js, react, redux with rxjs and docker from scratch with full code coverage.
We will create full stack app with node.js on backend side and react with redux and rxjs on frontend side. And redis will be our data storage. To run redis on dev PC I’ll show how easy to make it with docker.
Lets start creating our app from docker installation. After we should create directory and initiate node project. Who wants to see code now I created github repository.
Each new JavaScript project should start with package.json creating by using npm init command.
mkdir react-awesome-appcd react-awesome-appnpm init -f
Node.js now is the most popular platform not only as backend or tool for frontend bundling. As I wrote before many great companies support it for own services (Google, AWS) or use it as part of system (PayPal, Uber). For real production project it’s better to use cloud services like Google function or AWS lambda. But for development or small projects we can use node as http/https server with framework like express or koa. In the tutorial I’ll show you other framework — YEPS, I created it for working with promises, async / await, parallel execution…
As I said before to store our app data we can use redis. And thanks docker we can run it locally without dependencies and memory problems. To run it in docker we need to execute command
docker run -d --name redis -p 6379:6379 redis:alpine
Here we use small alpine image running as daemon (-d), with opened default port 6379. To stop it we need run:
docker rm -f redis
As we work with package.json we can add those commands to script section:
"scripts": {"db:start": "docker run -d --name redis -p 6379:6379 redis:alpine","db:stop": "docker rm -f redis"},
Don’t forget that package.json is json file so don’t break it. Now we just need to run npm run db:start to start redis service and npm run db:stop to stop it.
Don’t forget to install node.js version 7.6.0 and higher because we will use async / await natively. To check current version just run node -v (I use latest version thanks to nvm):
Now I’m going to install only one global dependency — ntl, it hepls to see all our commands:
npm i -g ntl
After just run ntl command in directory and select script:
It’s very important to understand how node.js http/https server works. It’s single process and in fact it’s EventEmitter with request event. Each user’s request runs event handler and each node.js framework like express.js or koa.js is just handler to process user’s requests. So any side effect like variables out of this process can create memory leaks. Be careful.
For our project we will use YEPS framework. I created it for one high load project with async / await and parallel jobs using Promise.all. You can check benchmark. Thanks to promise model and parallel execution instead of middleware model with loop and running modules one by one I got speed in 10 times faster.
To use YEPS we need install all dependencies (run command with -S flag to store dependencies in package.json and -D to store it as devDependencies):
npm i -S yeps yeps-bodyparser yeps-cors yeps-error yeps-helmet yeps-logger yeps-redis yeps-router yeps-server yeps-static
And we can see changes in package.json:
"dependencies": {"yeps": "^1.0.0","yeps-bodyparser": "^1.1.0","yeps-cors": "^1.0.0","yeps-error": "^1.2.0","yeps-helmet": "^1.0.0","yeps-logger": "^1.0.0","yeps-redis": "^1.0.0","yeps-router": "^1.0.0","yeps-server": "^1.0.0","yeps-static": "^1.2.0"}
To assamble our server part I’ll create new directory server and index.js as entry point.
mkdir servercd servertouch index.js
And put this code:
const App = require('yeps');const Router = require('yeps-router');const error = require('yeps-error');const logger = require('yeps-logger');const redis = require('yeps-redis');const bodyParser = require('yeps-bodyparser');const helmet = require('yeps-helmet');const cors = require('yeps-cors');const serve = require('yeps-static');
const app = module.exports = new App();const router = new Router();
router.get('/data').then(async (ctx) => {const rows = await ctx.redis.get('data');ctx.res.end(JSON.stringify(rows));}).post('/data').then(async (ctx) => {await ctx.redis.set('data', JSON.stringify(ctx.request.body));ctx.res.end(JSON.stringify({message: 'ok',}));});
app.all([error({ isJSON: true }),logger(),]).then(serve(),).all([redis(),bodyParser(),helmet(),cors(),]).then(router.resolve(),);
Here after require all dependencies I created app and router. In router I created two entry points to get and set data. It’s not restfull app.
The most interesting things we can see after. With YEPS we can run parallel all jobs but using promise interface (behind YEPS interface we work with promise) we can use priorities. First we should setup our logger and error handler (with json response headers). After checking request and understand that it’s not request to get static files like JavaScript files or index.html (without parameters this module looks into public directory) we can enable all other modules and run it parallel. After connecting to redis, setting all secure headers we can start parsing request and run router handler.
I use the same idea of parallel jobs in YEPS router. For example in express.js we need to think which routes are most important, popular and put it first. In YEPS router all checks work parallel. And first matching router will stop this process. If you need to make some priority you can create more than one router:
const routerFirst = new Router();const routerSecond = new Router();
app.then(routerFirst.resolve()).then(routerSecond.resolve());
Don’t forget to create config for our redis. Examples you can find in yeps-redis documentation. To do it we need to create config directory and put default.json file:
{"redis": {"host": "127.0.0.1","port": 6379}}
To run our server we need to create entry point and script in package.json like we made it for docker:
"start: server": "node ./bin/www",
and bin/www:
#!/usr/bin/env node
const app = require('../server');const server = require('yeps-server');
server.createHttpServer(app);
And command for running: npm run start:server (or use ntl).
After open browser http://localhost:3000/data and check response.
Now we need to instal our first dev dependencies. For testing we will use mocha, chai expect, sinon and chai-http:
npm i -D mocha chai chai-http sinon
And our package.json after has devDependencies:
"devDependencies": {"chai": "^4.1.2","chai-http": "^3.0.0","mocha": "^3.5.3","sinon": "^3.3.0"}
And to run test we need to add script to package.json:
"test:server": "mocha tests --recursive"
After that create test directory with servers directory and index.js file inside:
mkdir testscd testsmkdir servercd servertouch index.js
And code of our backend tests:
const chai = require('chai');const chaiHttp = require('chai-http');const sinon = require('sinon');const yeps = require('yeps-server');const redis = require('yeps-redis/redis');const logger = require('yeps-logger/logger');
const app = require('../../server');
const expect = chai.expect;chai.use(chaiHttp);let server;
describe('Server testing', () => {logger.info = text => text;logger.error = text => text;
beforeEach(() => {
server = yeps.createHttpServer(app);
});
afterEach(() => {
server.close();
});
it('should test 404 error', **async** () => {
**const** spy = sinon.spy();
**await** chai.request(server)
.get('/404')
.send()
.catch((error) => {
expect(error).to.have.status(404);
spy();
});
expect(spy.calledOnce).to.be.true;
});
it('should test static server', **async** () => {
**const** spy = sinon.spy();
**await** chai.request(server)
.get('/index.html')
.send()
.then((res) => {
expect(res).to.have.status(200);
spy();
});
expect(spy.calledOnce).to.be.true;
});
it('should test get data', **async** () => {
**const** spy = sinon.spy();
**await** redis.set('data', 'test');
**await** chai.request(server)
.get('/data')
.send()
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.be.equal('test');
spy();
});
expect(spy.calledOnce).to.be.true;
});
it('should test set data', **async** () => {
**const** spy = sinon.spy();
**const** data = 'test';
**await** chai.request(server)
.post('/data')
.send({ data })
.then((res) => {
expect(res).to.have.status(200);
spy();
});
**const** storedData = JSON.parse(**await** redis.get('data'));
expect(storedData.data).to.be.equal(data);
expect(spy.calledOnce).to.be.true;
});
});
And after running npm run test:server we see:
I disabled logger to see clear result (we can see info about 404 error test — page not found):
logger.info = text => text;logger.error = text => text;
That’s all for our backend. We created two endpoints to get and set our data and our server can be a static server. Now we are ready to start our frontend part in next chapter.
And some small updates if you work in team. First is editorconfig. It helps to use the same code standard for all team members. We need to create .editorconfig:
root = true
[*]end_of_line = lfinsert_final_newline = true
charset = utf-8
indent_style = spaceindent_size = 2
Next step is installing nsp — Node Security Platform. It helps you keep your node applications secure. We need it only for development so install it with -D flag:
npm i -D nsp
And add script to package.json to run it:
"test:security": "nsp check"
Next step is installing eslint and istanbul. Eslint helsp with ES6 code standards, we will use it with airbnb react plugin. And as we will use ES6 and higher we need to install new version of istanbul — istanbul@next.
npm i -D eslint eslint-config-airbnb eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-import istanbul@next
To work with eslint we need to create config file — .eslintrc:
{"extends": "airbnb","env": {"node": true,"mocha": true },"rules": {"jsx-a11y/href-no-hash": 0,"no-unused-expressions": 0,"no-multi-assign": 0}}
And script:
"lint:js": "eslint server tests",
To run: npm run lint:js
The same for istanbul — .istanbul.yml:
instrumentation:default-excludes: trueexcludes: []include-all-sources: true
And script:
"test:coverage": "istanbul cover _mocha -- tests --recursive"
And after running npm run test:coverage we see
To run all our command in one click we need npm-run-all package:
npm i -S npm-run-all
And command npm run test (short version is npm test or even npm t) will run all our tests:
"test": "npm-run-all test:**",
And final thing — each github repository should have Readme.md file with instruction how to install and how to use your project. Lets check github repository. And in next chapter I’ll show you how easy to create real reactive awesome app.