DEV Community

Hossomi
Hossomi

Posted on • Updated on

Frontline: Running Something

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
Enter fullscreen mode Exit fullscreen mode

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"))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
  [...]
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

This should be easy if you already know React hooks. Here is a quick explanation anyway:

  1. We use useState to define a state variable text with initial value Loading.... setText should be used to update its value.
  2. We use useEffect with an empty array in the second argument to run an effect only once when the component is mounted.
  3. This effect makes the request using fetch, parses the response as text using and store it in the text state variable, triggering a component re-render since the state changed.
  4. The actual component renders text on screen, which will now be Hello world.

However, if you start the server and open the client, you will see this:

Client without proxying displays an HTML instead of the server message

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
  # [...]
Enter fullscreen mode Exit fullscreen mode

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:

Client with proxying displays the server message

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!'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

We can now change our server's index.ts like this:

import { GREETING } from '@tamiyo/shared'

// [...]

app.get('/api', (req, res) => {
    res.send(GREETING)
})
Enter fullscreen mode Exit fullscreen mode

And the client App.tsx like this:

import { GREETER } from '@tamiyo/shared'

// [...]

<p>
  {GREETER}: {text}
</p>
Enter fullscreen mode Exit fullscreen mode

After rebuilding and restarting our applications, we can verify that both client and server are using the variables from the shared library:

Client and server uses 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"
  },
  # [...]
Enter fullscreen mode Exit fullscreen mode

We also need to define a start script in server/package.json:

{
  "name": "@tamiyo/server",
  # [...]
  "scripts": {
    "start": "yarn node build/index.js"
  },
  # [...]
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)