Creating a web application using Yarn workspace, TypeScript, esbuild, React, and Express — part 1
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.
- Setting up the project — part 1
- Adding code — part 2
- Building the app — part 3
- 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 amongapp
andserver
.
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:
- Create the project's folder with
mkdir my-app
(feel free to choose the name you want). - Navigate to it with
cd my-app
. - Initialize it with
yarn init
. This will prompt you with questions to create the initialpackage.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 theyarn init
command you can always create the file manually and copy the content below into it:
1{ 2 "name": "my-app", 3 "version": "1.0.0", 4 "license": "UNLICENSED", 5 "private": true // Required for yarn workspace to work 6}
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:
1my-app/ 2├─ packages/ // Where all our current and future modules will live 3│ ├─ app/ 4│ ├─ common/ 5│ ├─ server/ 6├─ 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:
1{ 2 "name": "@my-app/app", 3 "version": "0.1.0", 4 "license": "UNLICENSED", 5 "private": true 6}
The common
package:
1{ 2 "name": "@my-app/common", 3 "version": "0.1.0", 4 "license": "UNLICENSED", 5 "private": true 6}
The server
package:
1{ 2 "name": "@my-app/server", 3 "version": "0.1.0", 4 "license": "UNLICENSED", 5 "private": true 6}
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).
1{ 2 "name": "my-app", 3 "version": "1.0", 4 "license": "UNLICENSED", 5 "private": true, 6 "workspaces": ["packages/*"] // Add this here 7}
Your final folder structure should look like so:
1my-app/ 2├─ packages/ 3│ ├─ app/ 4│ │ ├─ package.json 5│ ├─ common/ 6│ │ ├─ package.json 7│ ├─ server/ 8│ │ ├─ package.json 9├─ 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 thedevDependencies
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 toapp
,common
, andserver
.
Your package.json
should look like:
1{ 2 "name": "my-app", 3 "version": "1.0", 4 "license": "UNLICENSED", 5 "private": true, 6 "workspaces": ["packages/*"], 7 "devDependencies": { 8 "typescript": "^4.2.3" 9 } 10}
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:
1{ 2 "compilerOptions": { 3 /* Basic */ 4 "target": "es2017", 5 "module": "CommonJS", 6 "lib": ["ESNext", "DOM"], 7 8 /* Modules Resolution */ 9 "moduleResolution": "node", 10 "esModuleInterop": true, 11 12 /* Paths Resolution */ 13 "baseUrl": "./", 14 "paths": { 15 "@my-app/*": ["packages/*"] 16 }, 17 18 /* Advanced */ 19 "jsx": "react", 20 "experimentalDecorators": true, 21 "resolveJsonModule": true 22 }, 23 "exclude": ["node_modules", "**/node_modules/*", "dist"] 24}
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:
1my-app/ 2├─ node_modules/ 3├─ packages/ 4│ ├─ app/ 5│ │ ├─ package.json 6│ ├─ common/ 7│ │ ├─ package.json 8│ ├─ server/ 9│ │ ├─ package.json 10├─ package.json 11├─ tsconfig.json 12├─ 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.
1{ 2 "name": "my-app", 3 "version": "1.0", 4 "license": "UNLICENSED", 5 "private": true, 6 "workspaces": ["packages/*"], 7 "devDependencies": { 8 "typescript": "^4.2.3" 9 }, 10 "scripts": { 11 "app": "yarn workspace @my-app/app", 12 "common": "yarn workspace @my-app/common", 13 "server": "yarn workspace @my-app/server" 14 } 15}
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.
1# Logs 2yarn-debug.log* 3yarn-error.log* 4 5# Binaries 6node_modules/ 7 8# Builds 9dist/ 10**/public/script.js
Your folder structure should look like below:
1my-app/ 2├─ packages/ 3├─ .gitignore 4├─ package.json
Posted on March 11, 2021