Simple Monorepo Setup With React.js And Express.js

March 14, 2024

simple monorepo setup with react.js and express.js

A simple setup that can help you get started with a monorepo using React and Express (SPA and API).

You can find the repo on GitHub.
It also includes things like ESLint, Prettier, Jest (Testing Library on the frontend and Supertest on the backend), and Tailwind.

1. Initialise the project

Create a new directory and navigate to it. Then run npm init in the terminal.

npm init

This should create a package.json file.

Install the first dependency - concurrently - it would help you run the backend and the frontend at the same time.

npm install concurrently -D

in the package.json file, define the workspaces.

"workspaces": [
  "backend",
  "frontend"
],

Your package.json should look something like this:

{
  "name": "react-express-starter",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "workspaces": ["backend", "frontend"],
  "devDependencies": {
    "concurrently": "^8.2.2"
  }
}

If you are using VS Code, you can create a workspace config file for the editor.

${nameOfYourRepo}.code-workspace - in this case react-express-starter.code-workspace

{
  "folders": [
    {
      "path": "./",
      "name": "project-root"
    },
    {
      "path": "./backend",
      "name": "backend"
    },
    {
      "path": "./frontend",
      "name": "frontend"
    }
  ]
  // you can add other settings here
}

2. Set up the backend

mkdir backend
cd backend
npm init -y

Go back to the root directory.

cd ..

Install express in the backend folder.

npm install express -w backend

express should appear in the package.json file in the backend folder.

Dev dependencies

Install a few essential dev dependencies like ESLint, Prettier, and TypeScript stuff (types and ts-node)

(If you don't want to use ESLint, Prettier, and TypeScript, you can skip this step)

npm i -D typescript ts-node eslint prettier eslint-config-prettier eslint-plugin-node eslint-plugin-prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser @types/express -w backend

Add config files for ESLint, Prettier, and TypeScript. You can find them in the GitHub repo.

Backend code

Now you can add some backend code, and set up the Express app.

This example contains a basic folder structure that can help you start when building something a bit more complex than an app with one or two routes.

mkdir services controllers routes

Create a service file that returns some dummy data and simulates a delay as if we are fetching data from an API or a database.

Create a services/book.service.ts file.

import books from '../booksData'

export const getAllBooks = async () => {
  // simulate a delay
  const data = await new Promise(resolve => setTimeout(() => resolve(books), 500))

  return data
}

You can find the booksData file in the GitHub repo.

Create a controllers/book.controller.ts file.

import { Request, Response } from 'express'

import { getAllBooks } from '../services/book.service'

export const all = async (req: Request, res: Response) => {
  try {
    const result = await getAllBooks()
    return res.status(200).send(result)
  } catch (error) {
    console.log(error)
    return res.status(500).send({ message: 'Something went wrong' })
  }
}

Create a routes/book.route.ts file.

import express from 'express'

import { all } from '../controllers/book.controller'

const router = express.Router()

router.get('/', all)

export default router

The final pieces are app.ts and server.ts files, and we can run the backend code.

Create a app.ts file.

import express from 'express'

import bookRouter from './routes/book.route'

const app = express()

app.use(express.json())

app.use('/books', bookRouter)

export default app

Create a server.ts file.

import app from './app'

const port = 4000

app.listen(port, () => {
  console.log(`App listening at http://localhost:${port}`)
})

Run the backend code

Add a dev script in the backend package.json file.

"scripts": {
  "dev": "ts-node server.ts"
}

Now you can run it.

npm run dev -w backend

Open http://localhost:4000/books in the browser.

Nodemon

If you change some file, you will have to restart the server manually to see updates. You can fix this with nodemon or similar.

npm i -D nodemon -w backend

Create a nodemon.json file in the backend folder.

{
  "exec": "ts-node --files server.ts",
  "ext": ".ts,.js"
}

Update the dev script in the backend package.json file.

"scripts": {
  "dev": "nodemon"
}

3. Set up the frontend

This example uses Vite for the frontend.

In the root folder run:

npm create vite@latest frontend

Then install the dependencies.

npm install -w frontend

In the frontend folder, you can see that ESLint is already set up. I also added Jest (Testing Library) and Tailwind, you can check it out in the GitHub repo.

Replace the code in the App.tsx to test things out.

import { useEffect, useState } from 'react'
import './App.css'

type Book = {
  id: string
  title: string
}

function App() {
  const [books, setBooks] = useState<Book[]>([])

  useEffect(() => {
    const getBooks = async () => {
      const response = await fetch('http://localhost:4000/books')
      const data = await response.json()
      setBooks(data)
    }
    getBooks()
  }, [])

  return (
    <>
      <ul>
        {books.map(book => (
          <li key={book.id}>{book.title}</li>
        ))}
      </ul>
    </>
  )
}

export default App

4. Run the frontend and the backend

In the package.json file in the root directory add these scripts:

"scripts": {
  "backend": "npm run dev -w backend",
  "frontend": "npm run dev -w frontend",
  "start": "concurrently \"npm run backend\" \"npm run frontend\""
},

Now if you run npm start in the root directory, that will run both the backend and the frontend.

if you open http://localhost:5173/ in the browser, you should see the list of books.

Visit the GitHub repo.