27
loading...
This website collects cookies to deliver better user experience
npx create-react-app cra-expressjs-docker --template typescript
npm i @material-ui/core
npm i react-router-dom @types/react-router-dom
axios
for http requests and react-json-view
to display a javascript objectnpm i axios react-json-view
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;
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;
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;
npm i @apollo/[email protected] [email protected]
npm i --save-dev @graphql-codegen/[email protected] @graphql-codegen/[email protected] @graphql-codegen/[email protected] @graphql-codegen/[email protected] @graphql-codegen/[email protected] @graphql-codegen/[email protected]
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
type DemoVisitor {
name: String!
id: Int!
message: String
}
"codegen": "gql-gen"
to scripts part in our package.json
npm run codegen
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
{
"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"]
}
module: "CommonJS"
nodejs modules are of CommonJS module type.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")
);
npm run build
in root directorybuild/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;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.query.name || "World";
res.json({
greeting: `Hello ${name}! From Expressjs on ${new Date().toLocaleString()}`,
});
});
package.json
as below;"scripts": {
"server:dev": "nodemon --exec ts-node --project tsconfig.json src/index.ts",
"server:build": "tsc --project tsconfig.json"
},
server:dev
does is to use ts-node
to start our Expressjs written in typescript according to tsconfig.json
. {
"watch": ["."],
"ext": "ts",
"ignore": ["*.test.ts"],
"delay": "3",
"execMap": {
"ts": "ts-node"
}
}
npm run server:dev
. If we update and save index.ts, server is supposed to be restarted.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
"proxy": "http://localhost:3001",
/api/greetins/hello
route. We can add another route for goodbye. Let's do this in a separate module;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;
DemoVisitor
model, which we already generated by GraphQL Code Generator in our client side, here on server side! Nice isn't it ?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")
);
npm run server:dev
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;
REACT_APP_TOOLBAR_COLOR
variable to window scope. window.REACT_APP_TOOLBAR_COLOR='red';
<!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>
<script src="/env-config.js"></script>
REACT_APP_TOOLBAR_COLOR
value.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}>
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.Compose up
.#!/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
#!/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
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!server:build
script, lets try it out with npm run server:build
. It produces javascript codes from typescript;Dockerfile
in the root folder to craete docker image of our app;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"]
**/node_modules
/build
/server/dist
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.ENTRYPOINT ["/app/build/docker-entrypoint.sh"]
is our entry point. #!/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
env-config.js
file with the output of the execution of generate_config_js.sh
and starts the node server.Build image...
. If everything goes well, docker image is built as craexpressjsdocker:latest
. docker-compose.yaml
file to run the docker image. Here we supply TOOLBAR_COLOR
environment variable too.version: "3.4"
services:
client:
image: craexpressjsdocker:latest
ports:
- "3001:3001"
environment:
TOOLBAR_COLOR: "purple"
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!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.