Frontline: Running Something
Hossomi
Posted on November 5, 2022
Hello everyone! You have probably noticed by now that I am not a consistent writer. It is finally time to run and see something!
Today we will implement the most basic of all: a CRA client talking to an HTTP server to display something.
In this article | Repository | Changes | Final commit
Server
The server is the simplest piece: an Express HTTP server that returns Hello world
on GET. We start by installing Express in the server workspace:
yarn workspace @tamiyo/server add express
Then we write the server in index.ts
:
import express from 'express'
const app = express()
app.get('/api', (req, res) => {
res.send("Hello world")
})
app.listen(8000, () => console.log("Listening at port 8000"))
The basics of Express is really simple and intuitive, so I probably don't need to explain the code above. To run, we can build and manually run the built script:
yarn tsc --build
yarn workspace @tamiyo/server node build/index.js
The first command will transpile Typescript into Javascript as we discussed in the last article. The second will run the transpiled Javascript file using Yarn's module resolution, required since we use workspaces.
We can test our server with curl
:
$ curl http://localhost:8000/api
Hello world
Client
The client is a basic CRA application that we will modify to get a message to display from our server.
⚠️ First of all, we will disable Yarn's Plug'n'Play (PNP) resolution:
yarn set config nodeLinker node yarn install
Although there are sources on the internet saying that it should, I could not make Workspaces + PNP + CRA work properly. I decided to not spend more time on that and focus on actual development.
We will replace our current client with code generated with CRA:
rm -r client
yarn create react-app client --template typescript
Our old client had some important configuration discussed in the last article. In package.json
, the name was changed and type
was removed. Put them back:
{
"name": "@tamiyo/client", # <-- Change back
"version": "0.1.0",
"private": true,
"type": "module", # <-- Put back
[...]
In tsconfig.json
, we extracted most of the properties to tsconfig.common.json
. So we can revert it, keeping only the few additional properties:
{
"extends": "../tsconfig.common.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "build",
# Additional properties:
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"noEmit": true,
"jsx": "react-jsx"
}
}
With that, you should be able to run the template and run it in your browser (CRA will open itself) by running the start
script with Yarn:
yarn workspace @tamiyo/client start
Integrating
The last step for today is modifying our client to make a request to our server and display the response on screen. This request should be made by the App
component:
function App() {
const [text, setText] = useState("Loading...")
useEffect(() => {
fetch('/api')
.then(res => res.text())
.then(setText)
}, [])
return (
<div className="App">
// [...]
<p>
{text}
</p>
// [...]
</div>
)
}
This should be easy if you already know React hooks. Here is a quick explanation anyway:
- We use
useState
to define a state variabletext
with initial valueLoading...
.setText
should be used to update its value. - We use
useEffect
with an empty array in the second argument to run an effect only once when the component is mounted. - This effect makes the request using
fetch
, parses the response as text using and store it in thetext
state variable, triggering a component re-render since the state changed. - The actual component renders
text
on screen, which will now beHello world
.
However, if you start the server and open the client, you will see this:
This happens because the client is actually making the request to the CRA development server that is serving the client pages, and not our server! In production these two servers would be the same, but we are not there yet.
CRA provides a solution for this in development: proxying. We can easily enable this in client/package.json
:
{
"name": "@tamiyo/client",
"version": "0.1.0",
"private": true,
"type": "module",
"proxy": "http://localhost:8000", # <-- Proxy to here
# [...]
With this, the CRA development server will proxy unknown requests (i.e. that are not requesting a client page) to our server that is running on port 8000. Restarting the client and trying again, you should finally see this:
The shared library
Remember we have a shared
workspace, where we will place common code? Now is a good time to link it to both client and server. For starters, let's just define two variables in shared/index.ts
:
export const GREETER = 'Hossomi'
export const GREETING = 'Hello world!'
GREETER
will be used by the client and GREETING
will be returned by the server. Since they are all Yarn workspaces and Yarn handles them as packages, we can install the shared library as any other package:
$ yarn workspace @tamiyo/server add @tamiyo/shared
$ yarn workspace @tamiyo/client add @tamiyo/shared
A small change in shared/package.json
is also necessary so that Typescript can correctly find sources inside the shared library module:
{
"name": "@tamiyo/shared",
"version": "1.0.0",
"packageManager": "yarn@3.2.0",
"main": "build/index.js" # <-- Add this
}
We can now change our server's index.ts
like this:
import { GREETING } from '@tamiyo/shared'
// [...]
app.get('/api', (req, res) => {
res.send(GREETING)
})
And the client App.tsx
like this:
import { GREETER } from '@tamiyo/shared'
// [...]
<p>
{GREETER}: {text}
</p>
After rebuilding and restarting our applications, we can verify that both client and server are using the variables from the shared library:
Quality of life
Today, we had to run some quite verbose commands to start our client and server. To make our lives easier, we can put them in Yarn scripts in the parent package.json
:
{
"name": "tamiyo",
"version": "1.0.0",
# [...]
"scripts": {
"build": "yarn tsc --build --verbose",
"clean": "yarn tsc --build --clean",
"start:server": "yarn workspace @tamiyo/server start",
"start:client": "yarn workspace @tamiyo/client start"
},
# [...]
We also need to define a start
script in server/package.json
:
{
"name": "@tamiyo/server",
# [...]
"scripts": {
"start": "yarn node build/index.js"
},
# [...]
We can now run yarn build
to transpile Typescript, yarn start:server
to start the server and yarn start:client
to start the client, much shorter commands! CRA even has hot reload in development mode, so changing client code while it is running will automatically update in the browser. I will investigate how to do the same for the server.
Next step
We have a basic client-server application running, but we are using plain HTTP. In the next step, I will get started on GraphQL. It is a new world to me, so I am not sure an entire article will be needed. If it turns out simpler than I expect, I can also explore MTG JSON data sets.
Posted on November 5, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 7, 2023