Creating a web application using Yarn workspace, TypeScript, esbuild, React, and Express (part 1)

Posted on March 11, 2021

This article will guide you through setting up a basic web application using Yarn's workspace, TypeScript, esbuild, Express, and React. At the end of this tutorial you will have a fully buildable and deployable web application.

  1. Setting up the project (part 1)
    1. Workspaces
    2. TypeScript
    3. Adding the first scripts
    4. Preparing fot Git
  2. Adding code (part 2)
  3. Building the app (part 3)
  4. Going further (advanced)

Want to see the full code? Check out the repository on GitHub at halftheopposite/tutorial-app.


This project will be structured as a monorepo. The goal of a monorepo is to improve the amount of code shared between modules and to better anticipate how those modules communicate together (in a micro-service architecture for example). For the purpose of this exercise we will keep the structure simple:

  • An app, which will represent our React website.
  • A server, which will serve our app using Express.
  • And common, where some code will be shared among app and server.

The only requirement before setting up the project is to have yarn installed on your machine. Yarn is a package manager in the same way as npm, but with better performances and slightly more features. You can read more on how to install it on the official documentation.

Workspaces

Navigate to the folder where you want to initialize the project, and follow these steps through your favourite terminal:

  1. Create the project's folder with mkdir my-app (feel free to choose the name you want).
  2. Navigate to it with cd my-app.
  3. Initialize it with yarn init. This will prompt you with questions to create the initial package.json file (do not worry, you can modify this at any time once the file has been created). If you don't want to use the yarn init command you can always create the file manually and copy the content below into it:
{
  "name": "my-app",
  "version": "1.0.0",
  "license": "UNLICENSED",
  "private": true // Required for yarn workspace to work
}

Now that the package.json file has been created, we need to create the folders for our modules app, common, and server. To facilitate the discovery of modules by yarn workspace and improve project's readability, we will nest our modules under a packages folder:

my-app/
├─ packages/ // Where all our current and future modules will live
│  ├─ app/
│  ├─ common/
│  ├─ server/
├─ package.json

Each of our module will act as a small and independent project and will require its own package.json to manage dependencies. To setup each one of them we can either use yarn init (in each folder) or create the files manually (through the IDE for example).

The naming convention used for the packages' names is to prefix each one of them with @my-app/*. This is called a scope in the NPM land (you can read more here). You do not have to prefix yours like so, but it greatly helps later on.

Once you have created and initialized all three packages you should have something quite similar as below.

The app package:

{
  "name": "@my-app/app",
  "version": "0.1.0",
  "license": "UNLICENSED",
  "private": true
}

The common package:

{
  "name": "@my-app/common",
  "version": "0.1.0",
  "license": "UNLICENSED",
  "private": true
}

The server package:

{
  "name": "@my-app/server",
  "version": "0.1.0",
  "license": "UNLICENSED",
  "private": true
}

Finally, we need to tell yarn where to look for the modules, so go back and edit the project's package.json file and add the following workspaces property (If you want to know more about the details, go look at Yarn's documentation for workspaces).

{
  "name": "my-app",
  "version": "1.0",
  "license": "UNLICENSED",
  "private": true,
  "workspaces": ["packages/*"] // Add this here
}

Your final folder structure should look like so:

my-app/
├─ packages/
│  ├─ app/
│  │  ├─ package.json
│  ├─ common/
│  │  ├─ package.json
│  ├─ server/
│  │  ├─ package.json
├─ package.json

You're now done setting up the basics for our project.

TypeScript

We will now add our first dependency to our project: TypeScript. TypeScript is a superset of JavaScript that implements types checking at build time.

Go to the project's root folder through your terminal and run yarn add -D -W typescript.

  • Argument -D adds TypeScript to the devDependencies since we only it during development and build time.
  • Argument -W allows a package to be installed at the workspaces root, making it available globally to app, common, and server.

Your package.json should look like:

{
  "name": "my-app",
  "version": "1.0",
  "license": "UNLICENSED",
  "private": true,
  "workspaces": ["packages/*"],
  "devDependencies": {
    "typescript": "^4.2.3"
  }
}

This will also create a yarn.lock file (which ensures throughout the life of the project that the intended versions of a dependency stay the same), and a node_modules folder which hold the binaries of our dependencies.

Now that we have TypeScript installed, a good practice is to tell it how to behave. To do so, we will add a configuration file that should be picked up by your IDE (automatically if you use VSCode).

At the root of the project create a tsconfig.json file and copy the content below into it:

{
  "compilerOptions": {
    /* Basic */
    "target": "es2017",
    "module": "CommonJS",
    "lib": ["ESNext", "DOM"],

    /* Modules Resolution */
    "moduleResolution": "node",
    "esModuleInterop": true,

    /* Paths Resolution */
    "baseUrl": "./",
    "paths": {
      "@flipcards/*": ["packages/*"]
    },

    /* Advanced */
    "jsx": "react",
    "experimentalDecorators": true,
    "resolveJsonModule": true
  },
  "exclude": ["node_modules", "**/node_modules/*", "dist"]
}

You can easily search for each of the compilerOptions properties and their actions, but the one that is the most useful to us is the paths property. This one tells TypeScript where to look for code and typings when using the @my-app/common import in the @my-app/server or @my-app/app packages for example.

Your current project structure should now look like this:

my-app/
├─ node_modules/
├─ packages/
│  ├─ app/
│  │  ├─ package.json
│  ├─ common/
│  │  ├─ package.json
│  ├─ server/
│  │  ├─ package.json
├─ package.json
├─ tsconfig.json
├─ yarn.lock

Adding the first script

Yarn workspace allows us to access any sub package through the yarn workspace @my-app/* command pattern, but typing the full command each time would become quite redundant. This is why, we can create a few helper script method that will ease our developer experience. Open the package.json at the root of the project and add the following scripts property to it.

{
  "name": "my-app",
  "version": "1.0",
  "license": "UNLICENSED",
  "private": true,
  "workspaces": ["packages/*"],
  "devDependencies": {
    "typescript": "^4.2.3"
  },
  "scripts": {
    "app": "yarn workspace @my-app/app",
    "common": "yarn workspace @my-app/common",
    "server": "yarn workspace @my-app/server"
  }
}

You can now execute any command as if you were in the sub-package. For example, you could add some new dependencies by typing yarn server add express. This would add new dependencies to the server package directly.

In the next part we will start building our frontend and backend applications.

Preparing fot Git

If you plan to use Git as your version control tool, it is highly recommended to ignore generated files such a binaries or logs.

To do so, create a new file named .gitignore at the root of the project and copy the following content into it. This will ignore some files that will be generated later on in this tutorial and avoid commiting huge load of unnecessary data.

# Logs
yarn-debug.log*
yarn-error.log*

# Binaries
node_modules/

# Builds
dist/
**/public/script.js

Your folder structure should look like below:

my-app/
├─ packages/
├─ .gitignore
├─ package.json

Like this article?

Have some questions? Email me at contact[at]halftheopposite.dev.