Using Expressjs as Backend for Create React App using Docker Compose
Tags: react, express, docker
Published: 2021-06-24 16:49:46

Creating an app using reactjs is really fascinating. You see it running on your developer machine and you're done! Really? Now you need to think about packaging, deployment, handling environment variables and sending request to your own backend.  Here we'll be going through these steps. Not going into the details of creating a Reactjs app. Completed application is in [Github repo](https://github.com/killjoy2013/cra-expressjs-docker).
Main motivation of creating such a development environment is to keep Create React App (CRA) intact and avoid creating external dependencies to any serverside technology. We'll sum up this consideration at the end.

##**Project Creation**
My nodejs version is 14.17.5

We'll create our Reactjs project with famous CRA starter;
`npx create-react-app cra-expressjs-docker --template typescript`

We'll use Material-Ui for a bare minimum ui design;
`npm i @material-ui/core`
  
Let's add React-Router for page navigation;
`npm i react-router-dom @types/react-router-dom`

Need to add `axios` for http requests and `react-json-view` to display a javascript object
`npm i axios react-json-view`

Let's add pages;

*src/pages/Greetings.tsx*
```jsx
import {
  Button,
  createStyles,
  Grid,
  makeStyles,
  Theme,
  Typography,
} from "@material-ui/core";
import TextField from "@material-ui/core/TextField";
import { useState } from "react";

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    grid: {
      margin: 20,
    },
    message: {
      margin: 20,
    },
  })
);

const Greetings = () => {
  const classes = useStyles({});
  return (
    <Grid
      className={classes.grid}
      container
      direction="column"
      alignItems="flex-start"
      spacing={8}
    >
      <Grid item>
        <TextField variant="outlined" size="small" label="Name"></TextField>
      </Grid>
      <Grid item container direction="row" alignItems="center">
        <Button variant="contained" color="primary">
          Say Hello
        </Button>
      </Grid>
    </Grid>
  );
};

export default Greetings;


```

*src/pages/Home.tsx*
```jsx
import {
  createStyles,
  Grid,
  makeStyles,
  Theme,
  Typography,
} from "@material-ui/core";
import React from "react";

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    grid: {
      margin: 20,
    },
  })
);

const Home = () => {
  const classes = useStyles({});
  return (
    <Grid className={classes.grid} container direction="row" justify="center">
      <Typography color="textSecondary" variant="h2">
        Welcome to Fancy Greetings App!
      </Typography>
    </Grid>
  );
};

export default Home;

```

and update App.tsx like below;
*src/App.tsx*
```jsx
import {
  AppBar,
  createStyles,
  makeStyles,
  Theme,
  Toolbar,
} from "@material-ui/core";
import { BrowserRouter, Link, Route, Switch } from "react-router-dom";
import Greetings from "./pages/Greetings";
import Home from "./pages/Home";

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    href: {
      margin: 20,
      color: "white",
    },
  })
);

const App = () => {
  const classes = useStyles({});
  return (
    <BrowserRouter>
      <AppBar position="static">
        <Toolbar>
          <Link className={classes.href} to="/">
            Home
          </Link>
          <Link className={classes.href} to="/greetings">
            Greetings
          </Link>
        </Toolbar>
      </AppBar>
      <Switch>
        <Route path="/greetings">
          <Greetings />
        </Route>
        <Route exact path="/">
          <Home />
        </Route>
      </Switch>
    </BrowserRouter>
  );
};

export default App;

```

Now our Reactjs app is ready. Although it lacks greetings funtionalities yet, you can still navigate between pages.
![init_app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/49mrb2vwk95snvg8hp3p.PNG)

##**Adding GraphQL Code Generator**
Although we're not going to add a GraphQL server for the time being, we can use GraphQL Code Generator to generate types to be used both in client side and also in server side. [GraphQL Code Generator](https://www.graphql-code-generator.com/) is a wonderful tool and definitely worth to get used to. 

Let's install necessary packages, `npm i @apollo/client@3.3.16 graphql@15.5.2 `

`npm i --save-dev @graphql-codegen/add@2.0.1 @graphql-codegen/cli@1.17.10 @graphql-codegen/typescript@1.17.10 @graphql-codegen/typescript-operations@1.17.8 @graphql-codegen/typescript-react-apollo@2.0.7 @graphql-codegen/typescript-resolvers@1.17.10`

Let's create two files;
*codegen.yml*
```yaml
overwrite: true
generates:
  ./src/graphql/types.tsx:
    schema: client-schema.graphql
    plugins:
      - add:
          content: "/* eslint-disable */"
      - typescript
      - typescript-operations
      - typescript-react-apollo
      - typescript-resolvers
    config:
      withHOC: false
      withHooks: true
      withComponent: false
```

*client-schema.graphql*
```graphql
type DemoVisitor {
  name: String!
  id: Int!
  message: String
}
```
also need to add `"codegen": "gql-gen"` to scripts part in our `package.json`

Now we can run codegenerator with `npm run codegen`

##**Adding Exressjs serverside using typescript**
Create a `server` directory in the root directory and `npm init -y` there. Then install the packages;

`npm i express ts-node typescript`
`npm i -D @types/express @types/node nodemon`

Since our server code is in typescript, it needs to be compiled to javascript. So, we need to instruct typescript compiler (tsc) somehow. You can do this by giving inline cli parameters. However, a more elegant way is to add a tsconfig file.

*server/tsconfig.json*
```json
{
  "compilerOptions": {
    "jsx": "react",
    "target": "es6",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "dist",
    "rootDirs": ["./", "../src/graphql"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": [".", "../src/graphql"]
}
```
What's important is `module: "CommonJS"` nodejs modules are of CommonJS module type.

Let me remind you, our goal is to keep CRA intact, just add serverside to it.

And add our server app;
*server/src/index.ts*
```ts
import express from "express";
import path from "path";

const app = express();
app.use(express.json());

const staticPath = path.resolve(__dirname, "../build/static");
const buildPath = path.resolve(__dirname, "../build");
const indexPath = path.resolve(__dirname, "../build/index.html");

app.use("/", express.static(buildPath));
app.use("/static", express.static(staticPath));

app.all("/", (req, res) => {
  res.sendFile(indexPath);
});

app.post("/api/greetings/hello", (req, res) => {
  const name = (req.body.name || "World") as string;

  res.json({
    greeting: `Hello ${name}! From Expressjs on ${new Date().toLocaleString()}`,
  });
});

app.listen(3001, () =>
  console.log("Express server is running on localhost:3001")
);

```

Lets's build client side Reactjs app using `npm run build` in root directory


If you check `build/index.html` you can see some `script` tags that points to some compiled artifacts under `build/static`. In our `server/app/index.ts` we created below paths to be used;

```jsx
const staticPath = path.resolve(__dirname, "../build/static");
const buildPath = path.resolve(__dirname, "../build");
const indexPath = path.resolve(__dirname, "../build/index.html");

app.use("/", express.static(buildPath));
app.use("/static", express.static(staticPath));
```

Also we return index.html which contains our CRA app as below;

```tsx
app.all("/", (req, res) => {
  res.sendFile(indexPath);
});
```

And this is how we response POST requests;
```jsx
app.post("/api/greetings/hello", (req, res) => {
  const name = req.query.name || "World";
  res.json({
    greeting: `Hello ${name}! From Expressjs on ${new Date().toLocaleString()}`,
  });
});
```

Finally, we need scripts part to our server `package.json` as below;


```json
"scripts": {
    "server:dev": "nodemon --exec ts-node --project tsconfig.json src/index.ts",
    "server:build": "tsc --project tsconfig.json"    
  },
```
Basically what `server:dev` does is to use `ts-node` to start our Expressjs written in typescript according to `tsconfig.json`.  

For nodemon watch the changes in serverside typescript files and restart Expressjs automatically upon save, we need to add below configuration file to root directory;

*nodemon.json*
```json
{
  "watch": ["."],
  "ext": "ts",
  "ignore": ["*.test.ts"],
  "delay": "3",
  "execMap": {
    "ts": "ts-node"
  }
}
```
We can test our server with `npm run server:dev`. If we update and save index.ts, server is supposed to be restarted.

Since our CRA app is running on `localhost:3000` and Expressjs on `localhost:3001`, sending an http request from CRA app to Expressjs normally causes CORS problem. Instead of dealing with CORS, we have an option to tell CRA app to proxy http request to Expressjs in our development environment. To do that, we need to add `proxy` tag to our `package.json`

```json
"proxy": "http://localhost:3001",
```

###**Adding more routes to Expressjs**

We have a `/api/greetins/hello` route. We can add another route for goodbye. Let's do this in a separate module;

*server/src/routes/Greetings.ts*
```tsx
import express from "express";
import { DemoVisitor } from "../../../src/graphql/types";

const router = express.Router();

router.post("/hello", (req, res) => {
  const name = (req.body.name || "World") as string;
  const id = Number(req.body.id || 0);

  const myVisitor: DemoVisitor = {
    id,
    name,
    message: `Hello ${name} :-( From Expressjs on ${new Date().toLocaleString()}`,
  };

  res.json(myVisitor);
});

router.post("/goodbye", (req, res) => {
  const name = (req.body.name || "World") as string;
  const id = Number(req.body.id || 0);

  const myVisitor: DemoVisitor = {
    id,
    name,
    message: `Goodbye ${name} :-( From Expressjs on ${new Date().toLocaleString()}`,
  };

  res.json(myVisitor);
});

export default router;

```
Note that we're making use `DemoVisitor` model, which we already generated by GraphQL Code Generator in our client side, here on server side! Nice isn't it ?

And our index.ts become simplified;
*server/src/index.ts*
```tsx
import express from "express";
import path from "path";
import greetings from "./routes/Greetings";

const app = express();
app.use(express.json());

const staticPath = path.resolve(__dirname, "../static");
const buildPath = path.resolve(__dirname, "..");
const indexPath = path.resolve(__dirname, "../index.html");

app.use("/", express.static(buildPath));
app.use("/static", express.static(staticPath));

app.get("/*", (req, res) => {
  res.sendFile(indexPath);
});

app.use("/api/greetings", greetings);

app.listen(3001, () =>
  console.log("Express server is running on localhost:3001")
);

```
Let's check is the server still runs OK with `npm run server:dev`

Finally, we'll update Greetings.tsx to use its backend;

*src/pages/Greetings.tsx*

```tsx
import {
  Button,
  createStyles,
  Grid,
  makeStyles,
  Theme,
  Typography,
} from "@material-ui/core";
import TextField from "@material-ui/core/TextField";
import { useState } from "react";
import axios from "axios";
import { Visitor } from "graphql";
import { DemoVisitor } from "../graphql/types";
import ReactJson from "react-json-view";

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    grid: {
      margin: 20,
    },
    message: {
      margin: 20,
    },
  })
);

const Greetings = () => {
  const classes = useStyles({});
  const [name, setName] = useState("");
  const [helloMessage, setHelloMessage] = useState<DemoVisitor>({
    name: "",
    id: 0,
    message: "",
  });
  const [goodbyeMessage, setGoodbyeMessage] = useState<DemoVisitor>({
    name: "",
    id: 0,
    message: "",
  });

  const handleChange = (event: any) => {
    setName(event.target.value);
  };
  const handleHello = async (event: any) => {
    const { data } = await axios.post<DemoVisitor>(
      `/api/greetings/hello`,
      {
        name,
        id: 3,
      },
      {
        headers: { "Content-Type": "application/json" },
      }
    );

    setHelloMessage(data);
  };
  const handleGoodbye = async (event: any) => {
    const { data } = await axios.post<DemoVisitor>(
      `/api/greetings/goodbye`,
      {
        name,
        id: 5,
      },
      {
        headers: { "Content-Type": "application/json" },
      }
    );

    setGoodbyeMessage(data);
  };
  return (
    <Grid
      className={classes.grid}
      container
      direction="column"
      alignItems="flex-start"
      spacing={8}
    >
      <Grid item>
        <TextField
          variant="outlined"
          size="small"
          label="Name"
          onChange={handleChange}
        ></TextField>
      </Grid>
      <Grid item container direction="row" alignItems="center">
        <Button variant="contained" color="primary" onClick={handleHello}>
          Say Hello
        </Button>
        <ReactJson
          src={helloMessage}
          displayDataTypes={false}
          shouldCollapse={false}
        ></ReactJson>
      </Grid>
      <Grid item container direction="row" alignItems="center">
        <Button variant="contained" color="primary" onClick={handleGoodbye}>
          Say Goodbye
        </Button>
        <ReactJson
          src={goodbyeMessage}
          displayDataTypes={false}
          shouldCollapse={false}
        ></ReactJson>
      </Grid>
    </Grid>
  );
};

export default Greetings;
```


Now we have a fully functional isomorphic app. Let's now Dockerize it.

###**Handling Environment variables**

Our last task is to handle environment variables. A full fledged prod ready app is supposed to be controlled via its environment variables. If you bootstrap your reactjs app using a server side template, you can do it while you render the index.html. However, this is a different approach from using Create React App. Our main focus is to obey CRA structure and building our dev infrastructure this way.

Let's change the color of the app bar using an environment variable.

First, add a javascript file to hold our toolbar color environment variable with a default color red. We're simply adding `REACT_APP_TOOLBAR_COLOR` variable to window scope. 

*public/env-config.js*
```js
window.REACT_APP_TOOLBAR_COLOR='red';
```

We need to update index.html to use env-config.js

*public/index.html*
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <script src="/env-config.js"></script>
    
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    
  </body>
</html>
```

The only change is to add `<script src="/env-config.js"></script>`

Let's update our AppBar to use `REACT_APP_TOOLBAR_COLOR` value.

*src/App.tsx*
```tsx
const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    href: {
      margin: 20,
      color: "white",
    },
    appBar: {
      backgroundColor: window["REACT_APP_TOOLBAR_COLOR"],
    },
  })
);

const App = () => {
  const classes = useStyles({});
  return (
    <BrowserRouter>
      <AppBar position="static"  className={classes.appBar}>
```

We've just added appBar style and used it.
You may receive typescript compiler error saying `Element implicitly has an 'any' type because index expression is not of type 'number'`. We can add `"suppressImplicitAnyIndexErrors": true` to `tsconfig.json` to suppress this error.

Let's test what we did by right click to docker-compose.yaml and select `Compose up`.

You must have a red app bar now!

What we actually need to do is to control this toolbar color parameter using docker-compose.yaml environment variables.
We need to add two shell script files;

*generate_config_js.sh*
```shell
#!/bin/sh -eu
if [ -z "${TOOLBAR_COLOR:-}" ]; then
    TOOLBAR_COLOR_JSON=undefined
else
    TOOLBAR_COLOR_JSON=$(jq -n --arg toolbar_color "$TOOLBAR_COLOR" '$toolbar_color')
fi

cat <<EOF
window.REACT_APP_TOOLBAR_COLOR=$TOOLBAR_COLOR_JSON;
EOF

```

*docker-entrypoint.sh*
```shell
#!/bin/sh -eu
 echo "starting docker entrypoint" >&1
/app/build/generate_config_js.sh >/app/build/env-config.js
node /app/build/server
echo "express started" >&1
```

First shell script is to use TOOLBAR_COLOR environment variable which we'll be supplying in docker-compose.yaml.

Second one is to update our existing env-config.js with the first shell and start node server. 


###**Creating Docker image of our Application**

If your prod environment is a Kubernetes cluster, naturally you need to create a Docker image of your app. You should also decide how to respond to the initial http request to bootstrap your Reactjs app. Although adding `nginx` inside our image may seem reasonable, dealing with nginx configuration adds quite a lot intricacy to the scenario. Moreover, you're still lacking a backend in which you can create some business logic!

A far easier option can be using Expressjs as backend. This way, you avoid configuration issues, in addition, you will have a backend for frontend!

We already created our Expressjs and have a running full fledged app in dev mode. We can start to create our Docker image. 
First of all, let's remember, our ultimate purpose is not to make any change to CRA. It's innate build algorithm will be valid. We're just decorating our CRA with a backend.

We've already added `server:build` script, lets try it out with `npm run server:build`. It produces javascript codes from typescript;

You're supposed to have the output in a dist folder inside server folder;

![server-dist](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0x7t5gqz9eht2enfbcix.PNG)

Now we need to add a `Dockerfile` in the root folder to craete docker image of our app;

*Dockerfile*
```Dockerfile
FROM node:slim as first_layer

WORKDIR /app
COPY . /app

RUN npm install && \
    npm run build

WORKDIR /app/server
RUN npm install && \
    npm run server:build

FROM node:slim as second_layer

WORKDIR /app
COPY --from=client_build /app/build /app/build
COPY --from=client_build /app/public /app/public
COPY --from=client_build /app/server/dist/server/src /app/build/server
COPY --from=client_build /app/server/node_modules /app/build/server/node_modules

COPY --from=client_build /app/docker-entrypoint.sh /app/build/docker-entrypoint.sh
COPY --from=client_build /app/generate_config_js.sh /app/build/generate_config_js.sh

RUN apt-get update && \
    apt-get install dos2unix && \
    apt-get install -y jq && \
    apt-get clean

RUN chmod +rwx /app/build/docker-entrypoint.sh && \
    chmod +rwx /app/build/generate_config_js.sh && \
    dos2unix /app/build/docker-entrypoint.sh && \
    dos2unix /app/build/generate_config_js.sh

EXPOSE 3001
ENV NODE_ENV=production

ENTRYPOINT ["/app/build/docker-entrypoint.sh"]
```

*.dockerignore*
```
**/node_modules
/build
/server/dist
```
We have one Dockerfile and eventually we'll have a single Docker image which includes both client and server app. However, these two apps differ in terms of handling node_modules. When we build client app, CRA produces browser downloadable .js files. After that, we don't need node_modules. So, we should get rid of it not to bloat our docker image needlessly. On the other hand, at the end of the build process of the nodejs server app, we won't have a single .js file and node_modules directory should be kept for the server to run correctly!
So, we created a two layered dockerfile. In the first one, we install both client and server packages and also build them too. 
When we start the second layer, we copy only necessary artifacts from the first layer. At this point we could exclude node_modules of the CRA app.

After copying necessary files & directories, we need to install `dos2unix` and `jq` Ubuntu packages. While the former will be used to correct line endings of the shell files according linux, the latter is for json handling, in which we use in `generate_config_js.sh` file.

Second RUN command updates the file attributes by setting their chmod and correct the line endings.

Finally, `ENTRYPOINT ["/app/build/docker-entrypoint.sh"]` is our entry point. 

*docker-entrypoint.sh*
```shell
#!/bin/sh -eu
 echo "starting docker entrypoint" >&1
/app/build/generate_config_js.sh >/app/build/env-config.js
node /app/build/server
echo "express started" >&1
```

Basically, it creates `env-config.js` file with the output of the execution of `generate_config_js.sh` and starts the node server.

If you're using Docker in VS Code, definitely you would need to install 

![docker-vs-code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kslcy56uljg99uykb5cl.PNG)

It's an awesome extension and lets you monitor and perform all docker tasks without even writing docker commands.

Assuming you've installed the docker vscode extension, you can  right click Dockerfile and select `Build image...`. If everything goes well, docker image is built as `craexpressjsdocker:latest`. 

Now, let's add a `docker-compose.yaml` file to run the docker image. Here we supply `TOOLBAR_COLOR` environment variable too.
```yaml
version: "3.4"
services:
  client:
    image: craexpressjsdocker:latest
    ports:
      - "3001:3001"
    environment:
      TOOLBAR_COLOR: "purple"

```

Let's try it out. Just right click docker-compose.yaml and select `Compose up`. You must have your app running on `http://localhost:3001` with a purple pp bar. Let's change the toolbar color parameter in docker-compose.yaml to another color and again select Compose up. You must have your up with updated app bar color.  Congratulations!

###**Final words**

Let's recap what we have achieved;
* We added an Expressjs server side to a bare metal CRA app without ejecting or changing its base structure. We just decorated it with a server side. So, we can update the CRA any time in the future.

* Since we keep CRA as it is, development time is also kept unchanged. i.e., we still use webpack dev server and still have HMR. We can add any server side logic and create docker image as a whole app.

* We've encupsulated all the complexity in Docker build phase, in Dockerfile. So, development can be done without any extra issues. This makes sense from a developer's perspective to me. 

* Since our BFF (Backend For Frontend) is not a separate api hosted with a different URL, we don't need to deal with CORS issues, neighther need we create a reverse proxy.

* We have a ready-to-deploy docker image of our app to any Kubernetes cluster. 

* We can use environment variables in our CRA even though we did not use any server templating.

Happy coding 🌝