A modern React / Node.js application
Part 2: Setting up our Backend
Structure
Just like we created a client
folder in Part #1, we’ll create a server
directory now. In our top-level directory:
mkdir server && cd server/ && npm init --yes && mkdir src __tests__ && touch src/index.js
This will create our folder, and add another (empty) package.json
to it. Let’s add our productivity tools from Part #1:
npm i --save ramda immutable
npm i --save-dev eslint flow-bin
It would be great if we could re-use our previous .eslint.*
file from the client folder. Luckily, it’s super easy to do so. Just move the .eslint.*
file up and out of client
. In our top-level directory:
mv client/eslint.json .
Check to see that npm run lint
still works (you’ll need to cd
into client
, of course). And now we can use it to lint our server javascript as well.
Go with the flow
As mentioned previously, we’ll use the Flow tool to allow us to annotate our code with types, reducing the probability of silly bugs we’d otherwise catch (or not) at runtime. Let’s add that now. In our server/package.json
, add "flow": "flow"
under the scripts
property.
As we write code, we’ll annotate it with types, which Flow will check and warn us if we’re misstepping.
You can run
npm run flow
now to see the typechecker initialize itself, check our (nonexistent) code, and return “No errors!”
Build an Express API
We’ll use the old reliable of Node server libraries: Express. It’s simple, flexible, and ubiquitous. There are other libraries which may be more hip, but Express is the grizzled old veteran of the API wars. Its middleware capabilities are well-understood and many libraries have been written for things like request parsing, authentication, caching, logging, etc.
Let’s get started. In our server
directory:
npm i --save express body-parser
Now, in src/index.js
(which is empty), let’s add:
// @flow
'use strict'
import express from 'express'
import bodyParser from 'body-parser'
const app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
// we'll change these lines
import sum from './sum'
app.get('/sum', (req, res) => {
const { a, b } = req.query
res.status(200).send(sum(+a, +b).toString()) // coerce a and b into integers with +
})
app.listen(3001, () => {
/* eslint-disable no-console */
console.log('Listening on port 3001.')
/* eslint-enable no-console */
})
Additionally, create a sum.js
file in src
with contents:
export default function sum(a, b) {
return a + b
}
Now if we tried node src/index.js
at this point, Node would complain:
/Users/jroot/Dev/react-node-app/server/src/index.js:3
import express from 'express';
^^^^^^
SyntaxError: Unexpected token import
That’s because we need to use Babel to transpile ES6/7 code to the type of javascript code that Node understands (to be fair, Node understands quite a bit of ES6/7, but it does not yet support import
statements).
So let’s install Babel and its sub-dependencies:
npm i --save-dev babel-cli babel-eslint babel-plugin-transform-class-properties babel-plugin-transform-flow-strip-types babel-preset-env eslint-plugin-babel eslint-plugin-flow-header eslint-plugin-flowtype eslint-plugin-react babel-plugin-transform-object-rest-spread
Yeah… it’s quite a bit. Suffice it to say we’re installing the main Babel CLI package plus a bunch of Babel plugins, presets, and a few more eslint plugins.
Note, in newer versions of npm (>= 5?), a “lock file” is created by the above command:
npm notice created a lockfile as package-lock.json. You should commit this file.
We’ll take npm’s word for it and commit this file.
Now, we’ll add a .babelrc
file in our server
folder, which Babel needs in order to transpile our code:
touch .babelrc
Set .babelrc
’s contents to:
{
"plugins": [
"syntax-flow",
"transform-flow-strip-types",
"transform-class-properties",
"transform-object-rest-spread"
],
"presets": ["env"]
}
Now, run the following:
./node_modules/.bin/babel-node src/index.js
You should see that the server is now Listening on port 3001.
Try running curl localhost:3001/sum?a=1\&b=2
. You’ll see that 1 + 2 is in fact 3. Woohoo!
You might be thinking: “that’s a lot of work to get a Node app up and running.” Definitely. And it’s part of the boilerplate mentioned in Part #1, except this time we don’t have create-react-app
holding our hand to run it on the server. But, once it’s done, it’s done, and we can add commands to our package.json
’s scripts
property to make life easier:
"babel": "./node_modules/.bin/babel src --out-dir out",
"build": "npm run check && rm -rf out/ && npm run babel",
"check": "npm run lint && npm run flow",
"lint": "./node_modules/.bin/eslint src/ || true",
"flow": "flow; test $? -eq 0 -o $? -eq 2",
"babel-node": "./node_modules/.bin/babel-node src/index.js",
"watch": "nodemon --exec \"npm run babel-node\""
The last one, watch
, uses nodemon. If you don’t have it installed, do so with npm i -g nodemon
. This is the command we’ll use when we’re developing, as it restarts our app when it notices a change.
build
will copy the babel output to a folder out
(assuming we can npm run check
without issues). As such, we’ll add a line to our project’s .gitignore
: server/out
. This is so we don’t commit our build artifacts to source control.
The rest of the commands should be familiar and/or self-explanatory.
Where are the tests!?
We must not forget to test, so let’s install Jest (remember, we got it for free with create-react-app
in client
but we aren’t using create-react-app
here in server
, so we must be explicit).
npm i --save-dev jest
In our __tests__
folder, create a file index.js
, and set its conents to:
import sum from '../src/sum'
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
Then a simple ./node_modules/.bin/jest
should run our test. Let’s make life easier by editing our package.json
’s test
command under the scripts
property to look like so:
"test": "./node_modules/.bin/jest"
Now we can run our tests by doing npm test
(or even npm t
!).
Hitting the API from our app
A curl request is nice, but we’re building an app here. Let’s make the connection between our client and server. For now, we’ll assume that our app is a calculator that adds two numbers.
Would you look at that! We’ve already built that capability on the backend. How convenient.
So, let’s switch over to our client
project. We’ll need a way for the user to enter two numbers, and a way for the client to request that the server add these numbers and return the result. I can envision two inputs and a button. Change your App.js
to look like the following:
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import * as api from './api'
class App extends Component {
constructor() {
super()
this.state = {
a: 0,
b: 0,
sum: null,
error: null
}
}
render() {
return (
<div className="App">
<div>
<span>A</span>
<input type="number" defaultValue={this.state.a} onChange={e => this.setState({ a: e.target.value })} />
</div>
<div>
<span>B</span>
<input type="number" defaultValue={this.state.b} onChange={e => this.setState({ b: e.target.value })} />
</div>
<div>
<button onClick={() => this.handleClick(this.state.a, this.state.b)}>Calculate!</button>
</div>
<span>{this.state.sum === null ? '' : `The sum is ${this.state.sum}`}</span>
<span style=>{this.state.error}</span>
</div>
);
}
async handleClick(a, b) {
try {
const result = await api.sum(a, b)
this.setState({ sum: result, error: null })
}
catch (e) {
this.setState({ error: e.toString(), sum: null })
}
}
}
export default App;
As you can see, we have two inputs and a button. When the button is clicked, we’ll use the sum
import from our api.js
, which looks like this:
export function sum(a, b) {
return send(`http://localhost:3001/sum?a=${a}&b=${b}`)
}
async function send(url, options) {
const res = await fetch(url, options)
if (!res.ok) {
const err = new Error(await res.text())
return Promise.reject(err)
}
return res.text()
}
It’s worth noting here that we’re using the new
async
/await
syntax in ES7. This allows us to write asynchronous code (i.e., send a fetch request) as if it were synchronous. If the request fails, we return a rejected Promise, which is captured in thecatch
by the calling function (handleClick
). Nice!
Make sure you’re running both the client (npm start
) and the server (npm run watch
). Navigate to localhost:3000
, and (finally) enter numbers into the inputs and click Calculate!.
Whaaa? The astute observer might’ve already made the connection that an app running on localhost:3000
can’t send a request to localhost:3001
without the appropriate CORS headers.
Sure enough, we’ll see a bunch of red text in the browser’s console that looks like the following:
Fetch API cannot load http://localhost:3001/sum?a=-2&b=2. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access. The response had HTTP status code 500. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Everything’s OK, we just need to tweak our client/package.json
file a bit. create-react-app
sets up a service to serve our client app on localhost:3000
, but our backend server is running on localhost:3001
. Luckily, we can tell our client app to proxy requests over to our backend app by adding the following line to client/package.json
’s top level:
"proxy": "http://localhost:3001"
This is forces the client server to forward requests to our backend server.
One last change: in api.js
, remove the http://localhost:3001
part of the URL, so that it’s just /sum?a=${a}&b=${b}
.
Now, pressing the Calculate! button should display text indicating the result.
Now for a real app
This has been… uhh, fun. Sure. But the end result is that we have a client that can interact with our backend via an API. In our next post, we’ll create a realistic app that uses GraphQL to communicate with our backend. Keep reading!