Building a full stack React and Express site using Flow

Jul 31, 2018 15:21 · 2400 words · 12 minute read React Flow Express

After experiencing the joys of using FlowType with React, I wanted to have the same improved development experience on the backend. Unfortunately, this proved to be quite difficult. If you’re in this spot, then hopefully this post helps alleviate some of the pain.

Before we jump into how to do this, let’s discuss some of the difficulties first by examining a simple Express application built using Flow:

import express, {
  type $Application,
  type $Request,
  type $Response,
} from 'express';
import path from 'path';

const PORT: number = 8080;
const app: $Application = express();

app.use(express.static(path.join(__dirname, '..', '..', 'build')));

app.get('*', (req: $Request, res: $Response) => {
  res.sendFile(path.join(__dirname, '..', '..', 'build', 'index.html'));
});

app.listen(PORT, () => {
  logger.info(`Server listening on port ${PORT}`);
});

This is about as simple as it gets. If you have FlowType setup on your server and you put this code in src/server/index.js of a create-react-app site, this will serve your application. If you try to run this file, it will spew an incredible amount of errors.

The first error will be about ES6 imports. NodeJS doesn’t support them, so we will have to find a way around that. One widely accepted solution is to transpile the code using a tool called Babel. We’ll use this solution.

The second error will be about Flow declarations. NodeJS doesn’t support type definitions, so we will also have to find a way around that. We’ll use Babel again, since it has a Flow configuration preset that supports stripping out types.

And then, once we fix these errors, we’ll run into the issue of having reduced the effectiveness of our development environment. Using a transpiler will slow down the code slightly, which is frowned upon for production environments. We’ll have to setup a build step to compile the code, so that our production server doesn’t run into a negative performance impact.

Finally, our Flow backend doesn’t have the wonderful file watching that create-react-app provides out of the box. We’ll leverage the PM2 process manager, which provides a nice auto-restart functionality for our production environment, and also provides the file watching mechanism that we desire.

Let’s get into it!

Our approach will look something like this:

  1. Create a React application.
  2. Reorganize folder structure to accomodate a server, client, and shared code.
  3. Run development server using a Babel interpreter.
  4. Run production server from compiled Babel output.
  5. Create a folder for shared code.
  6. Build a simple REST API using shared types.
  7. Build a simple component to display data from the REST API.
  8. ?
  9. Profit.

Let’s do it.

Create a React application

Creating a React application that uses Flow requires some setup. I’ve blogged about this before, so instead of duplicating the instructions, you can check out that post here.

We’ll start from the project created at the end of that post.

Reorganize folder structure

Our goal is to have a single repository host the client and server code for our application. A default create-react-app project places all of the clientside files into a src/ directory. That’s great for a simple client application, but it’s a bit confusing when the source includes both server and client code. Let’s separate the code into client and server directories.

Open a terminal to the root of your project, then execute the following commands to create client and server folders:

mkdir src/client
mkdir src/server

Now let’s move everything except index.js into the client folder.

mv src/App* src/client/
mv src/logo.svg src/client/
mv src/registerServiceWorker.js src/client/
mv src/index.css src/client/

Great. You should now have only an index.js within your src directory, alongside a client folder and server folder.

If you run npm run flow, you’ll see some compilation errors:

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/index.js:3:8

Cannot resolve module ./index.css.

     1│ import React from 'react';
     2│ import ReactDOM from 'react-dom';
     3│ import './index.css';
     4│ import App from './App';
     5│ import registerServiceWorker from './registerServiceWorker';
     6│


Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/index.js:4:17

Cannot resolve module ./App.

     1│ import React from 'react';
     2│ import ReactDOM from 'react-dom';
     3│ import './index.css';
     4│ import App from './App';
     5│ import registerServiceWorker from './registerServiceWorker';
     6│
     7│ const root: ?HTMLElement = document.getElementById('root');


Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ src/index.js:5:35

Cannot resolve module ./registerServiceWorker.

     2│ import ReactDOM from 'react-dom';
     3│ import './index.css';
     4│ import App from './App';
     5│ import registerServiceWorker from './registerServiceWorker';
     6│
     7│ const root: ?HTMLElement = document.getElementById('root');
     8│



Found 3 errors

This is because index.js now references files that live in a different place. Let’s update the references by changing the contents of index.js to:

import React from 'react';
import ReactDOM from 'react-dom';
import './client/index.css';
import App from './client/App';
import registerServiceWorker from './client/registerServiceWorker';

const root: ?HTMLElement = document.getElementById('root');

if (root != null) {
  ReactDOM.render(<App />, root);
  registerServiceWorker();
}

If you run npm run flow again, you shouldn’t see any errors.

We didn’t move src/index.js into src/client/ because create-react-app doesn’t support updating the entry point directory. There are some workarounds to this that I haven’t included in this post in order to keep it focused on solving just one problem.

Building an Express Backend

Now that we have a nice structure for our server, let’s implement it. For demonstrative purposes, we’ll build a REST API that returns a helpful message, which our client will load and then render. This API will have the following URL structure:

  • GET /api/message -> { "message": "Hello, World!" }

Let’s use express to host our API. Install it:

npm i --save express body-parser

Create a file at src/server/index.js with the following contents:

import express, {
  type $Application,
  type $Request,
  type $Response,
} from 'express';
import path from 'path';

const PORT: number = process.env.PORT != null
  ? parseInt(process.env.PORT, 10)
  : 8080;

const app: $Application = express();

app.get('/api/message', (req: $Request, res: $Response): void => {
  res.json({
    message: 'Hello, World!',
  });
})

app.use(express.static(
  path.join(__dirname, '..', '..', 'build'),
  { index : false }
));

app.get('*', (req: $Request, res: $Response) => {
  res.sendFile(path.join(__dirname, '..', '..', 'build', 'index.html'));
});

app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});

This should satisfy our requirements, but if you try to run it…

node src/server/index.js

You get this:

(function (exports, require, module, __filename, __dirname) { import express, {
                                                              ^^^^^^

SyntaxError: Unexpected token import
    at createScript (vm.js:80:10)
    at Object.runInThisContext (vm.js:139:10)
    at Module._compile (module.js:576:28)
    at Object.Module._extensions..js (module.js:623:10)
    at Module.load (module.js:531:32)
    at tryModuleLoad (module.js:494:12)
    at Function.Module._load (module.js:486:3)
    at Function.Module.runMain (module.js:653:10)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3

It turns out that Node doesn’t support ES6 imports. The horrors. Fortunately, we can work around that by using babel to interpret our code for us in a development environment, and we can use it to compile ES6 code into ES5 for use in production. Let’s do it!

Setting up a Development Environment

We need to install babel as well as a couple presets to allow it to process ES6 and Flow code. Let’s also throw in a module resolver plugin so that we can import relative to the src/ directory rather than writing large relative import statements in our code. Run the following commands:

npm i --save-dev babel-cli babel-preset-env babel-preset-flow babel-plugin-module-resolver

Now that we have all the plugins we need, let’s configure babel by creating a .babelrc file in the root of our project and putting the following JSON object into it:

{
  "presets": ["env", "flow"],
  "plugins": [
    ["module-resolver", {
      "root": ["./src"]
    }]
  ]
}

This tells babel to use our env and flow plugins, and it configures the resolver plugin to resolve packages relative to the src/ directory. Wonderful.

Test that it worked by running the following command:

$(npm bin)/babel-node src/server/index.js

While this application is running, visit http://localhost:8080/api/message in a browser. You should see a helpful message.

We have babel configured, but it’s still tedious to run our server. Ideally, we’d like to run the development server by simply typing npm start. If you do that right now, it merely launches the clientside application, and it doesn’t do anything with our server.

Rather than simply changing the command to use babel-node, let’s rename the existing start script to start-client, and then create a new command named start-server that runs our server. Once we have these in place, we can use the handy npm-run-all package to run both the start-client and start-server scripts in parallel.

In addition to all of this, I also like having my development server automatically restart when it detects changes. (I’m very needy – I know.) I generally use the pm2 for this. Let’s do it!

Install pm2 and npm-run-all:

npm i --save pm2 npm-run-all

Let’s configure our pm2 development environment by creating a development.config.js file in the root of our project, and putting the following content into it:

module.exports = {
  apps: [{
    name: 'app-dev',
    script: './src/server/index.js',
    interpreter: 'babel-node',
    interpreter_args: '--presets env,flow',
    ignore_watch: [
      'src/client',
      'public',
    ],
    env: {
      NODE_PATH: 'src/',
      NODE_ENV: 'development',
    },
  }],
};

This tells pm2 where our server file entry point is, to use babel-node for running our server, to use the env and flow presets, and not to watch our client files.

Now let’s update our package.json to include our new commands:

{
  // ...
  "scripts": {
    "start": "npm-run-all --parallel start-server start-client",
    "start-client": "flow && react-scripts start",
    "start-server": "flow && pm2-dev start development.config.js",
    "build": "react-scripts build",
    "test": "flow && react-scripts test --env=jsdom",
    "eject": "react-scripts eject",
    "flow": "flow",
    "postinstall": "flow-typed install"
  },
  // ...
}

Try running this with npm start. You’ll be able to visit your React application at http://localhost:3000, and you’ll be able to visit your server at http://localhost:8080/api/message. Unfortunately, your clientside application cannot talk to your backend server, since it’s running on a different port. Let’s fix that by using an NPM proxy to redirect all /api requests to port 8080.

Add the follow to your package.json:

{
  // ...
  "proxy": {
    "/api": {
      "target": "http://localhost:8080"
    }
  }
}

Now you should be able to access your API at http://localhost:3000/api/message.

That should be enough for our development environment. Now let’s setup our production environment.

Setting up a Production Environment

Using babel-node in production may cause performance issues, so we’re going to use babel’s compiled output instead. Ideally, we will start our production server using the npm run serve command, but due to the compilation process we will need to build it first with npm run build. Turns out we have the same conflicts with the default create-react-app scripts, so we’ll need to split the build script into two.

Update your package.json again with the following scripts:

{
  // ...
  "scripts": {
    "start": "npm-run-all --parallel start-server start-client",
    "start-client": "flow && react-scripts start",
    "start-server": "flow && pm2-dev start development.config.js",
    "build": "npm-run-all build-client build-server",
    "build-client": "react-scripts build",
    "build-server": "babel src/ --ignore src/client,src/index.js -d dist",
    "test": "flow && react-scripts test --env=jsdom",
    "eject": "react-scripts eject",
    "flow": "flow",
    "postinstall": "flow-typed install"
  },
  // ...
}

Running npm run build will now build your clientside files into build, and it will build your server files into dist.

Before we go too far, let’s make sure to ignore our dist folder in Flow and Git.

Add the following to .gitignore:

/dist

Update your .flowconfig with the following:

[ignore]

.*/node_modules/.*
.*/build/.*
.*/dist/.*

[include]

[libs]

flow-typed

[lints]

[options]

all=true
module.system.node.resolve_dirname=node_modules
module.system.node.resolve_dirname=src
esproposal.decorators=ignore
esproposal.class_static_fields=enable
esproposal.class_instance_fields=enable
suppress_type=$FlowIssue
suppress_type=$FlowFixMe

[strict]

Let’s tell pm2 how to launch our compiled production application by creating a production.config.js in the root with the following contents:

module.exports = {
  apps: [{
    name: 'app',
    script: './dist/server/index.js',
    env: {
      NODE_PATH: 'dist/',
      NODE_ENV: 'production',
    },
  }],
};

Finally, add the serve script to your package.json:

{
  // ...
  "scripts": {
    "start": "npm-run-all --parallel start-server start-client",
    "start-client": "flow && react-scripts start",
    "start-server": "flow && pm2-dev start development.config.js",
    "build": "npm-run-all build-client build-server",
    "build-client": "react-scripts build",
    "build-server": "babel src/ --ignore src/client,src/index.js -d dist",
    "serve": "pm2-runtime start production.config.js",
    "test": "flow && react-scripts test --env=jsdom",
    "eject": "react-scripts eject",
    "flow": "flow",
    "postinstall": "flow-typed install"
  },
  // ...
}

Test it out by running the following:

npm run build
npm run serve

You should be able to visit your built production application at http://localhost:8080.

Hitting our REST API from React

Now that we have all of this setup, let’s build a simple React component to render messages from our API. Create a client service at src/client/api.js with the following contents:

export default {
  async message(): Promise<any> {
    const response = await fetch('/api/message');
    const data = await response.json();

    return {
      message: data.message,
    };
  },
};

Now create a component to render it at src/client/Message.js:

import * as React from 'react';
import api from './api';

export type Props = {};
export type State = {
  message: string,
};

export default class Message extends React.Component<Props, State> {
  state = {
    message: '',
  };

  async componentDidMount() {
    const response = await api.message();

    this.setState({
      message: response.message,
    });
  }

  render() {
    const { message } = this.state;

    return (
      <span>{message}</span>
    );
  }
}

And finally, update your src/client/App.js to use our new component:

import * as React from 'react';

import Message from './Message';

import logo from './logo.svg';
import './App.css';

type Props = {};

class App extends React.Component<Props> {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <Message />
      </div>
    );
  }
}

export default App;

If you run your application, you should see “Hello, World” displayed from your backend.

We’ve almost finished our application, but there’s one important piece still missing.

Sharing Types between Client and Server

Ideally, we want type checking across our client and server boundaries, but our API doesn’t currently enforce that, since the response from our service isn’t strongly typed. Let’s fix that by creating a shared directory to house our shared types.

mkdir src/shared

And let’s create our Message type in that folder at src/shared/types.js:

export type Message = {
  message: string,
};

Now let’s update our message route in src/server/index.js to use this type:

// ...
app.get('/api/message', (req: $Request, res: $Response): void => {
  const message: Message = {
    message: 'Hello, World!',
  };

  res.json(message);
})
// ...

And update our client service at src/client/api.js:

import { type Message } from 'shared/types';

export default {
  async message(): Promise<Message> {
    const response = await fetch('/api/message');
    const data = await response.json();
    
    const message: Message = {
      message: data.message,
    };

    return message;
  },
};

If you get an error that React can’t find the file ‘shared/types’, you may need to create a file named .env in the root containing: NODE_PATH=src/. This tells Node to look in the src directory for absolute package names.

Now try running your production server:

npm run build
npm run serve

And there you have it: a fully functioning React and Express application using Flow for the entire stack.

You can check out the final code here: https://github.com/FindAPattern/flow-backend.git.

Happy coding!

Tweet Share

Subscribe to my newsletter to receive updates about new posts.

* indicates required