Building a full-stack TypeScript application with Turborepo
Tags: typescript, mobile
Published: 2022-11-30 19:20:55

**Written by [Omar Elhawary](https://blog.logrocket.com/author/omarelhawary/)✏️**

Whether you're building a full-stack application or an application composed of multiple frontend and backend projects, you'll probably need to share parts across projects to varying extents. 

It could be types, utilities, validation schemas, components, design systems, development tools, or configurations. Monorepos help devs manage all these parts in one repository. 

In this article, we will provide an overview of what monorepos are and what the benefits are of using Turborepo. We'll then build a simple full-stack application using Turborepo with React and Node.js using [pnpm](https://blog.logrocket.com/managing-full-stack-monorepo-pnpm/) workspaces and demonstrate how the process can be improved by using Turborepo.

*   [What is a monorepo?](#what-is-a-monorepo)
    *   [Polyrepos](#polyrepos)
    *   [Optimizing monorepos](#optimizing-monorepos)
    *   [What is Turborepo?](#what-is-turborepo)
*   [Structuring the base monorepo](#structuring-the-basemonorepo)
    *   [Workspaces with pnpm](#workspaces-with-pnpm)
    *   [Structure overview](#structure-overview)
*   [Shared types package setup](#shared-types-package-setup)
*   [Backend setup (Express, TypeScript, `esbuild`, `tsx`)](#backend-setup-express-typescript-es-build-tsx)
*   [Frontend (React, TypeScript, Vite) setup](#frontend-react-typescript-vite-setup)
*   [Adding Turborepo](#adding-turbo-repo)
    *   [Installation and configuration](#installation-and-configuration)

## What is a monorepo?

A monorepo is a single repository that contains multiple applications and/or libraries. Monorepos facilitate project management, code sharing, cross-repo changes with instant type-checking validation, and more. 

Turborepo is one of the best monorepo tools in the JavaScript/TypeScript ecosystem. 

It's fast, easy to configure and use, independent from the application technologies, and can be adopted incrementally. It has a small learning curve and a low barrier to entry — whether you're just starting out with monorepos or are experienced and looking to try different tools in the ecosystem. 

Here's a representation of the structure of a monorepo and a polyrepo (source can be found [here](https://monorepo.tools/#what-is-a-monorepo)): ![Monorepo-Vs-Polyrepo](https://blog.logrocket.com/wp-content/uploads/2022/11/monorepo-vs-polyrepo.png)

### Polyrepos

Let's say we're building a full-stack application; both the frontend and the backend are two separate projects, each of them placed in a different repository — this is a polyrepo. 

If we need to share types or utilities between the frontend and the backend and we don't want to duplicate them on both projects, we have to create a third repository and consume them as an external package for both projects. 

Each time we modify the shared package, we have to build and publish a new version. Then, all projects using this package should update to the newest version. 

In addition to the overhead of versioning and publishing, these multiple parts can quite easily become out of sync with a high possibility of frequent breakages. 

There are other shortcomings to polyrepos depending on your project, and using a monorepo is an alternative that addresses some of these issues.

### Optimizing monorepos

Using monorepos without the right tooling can make applications more difficult to manage than using polyrepos. To have an optimized monorepo, you'll need a caching system along with optimized task execution to save development and deployment time. 

There are many tools like [Lerna](https://lerna.js.org), [Nx](https://nx.dev), [Turborepo](https://turborepo.org), [Moon](https://moonrepo.dev), [Rush](https://rushjs.io), and [Bazel](https://bazel.build), to name a few. Today, we'll be using Turborepo, as it's lightweight, flexible, and easy to use. 

You can learn more about monorepos, when and why to use them, and a comparison between various tools at [monorepo.tools](https://monorepo.tools).

### What is Turborepo?

Turborepo is a popular monorepo tool in the JavaScript/TypeScript ecosystem. It's written in Go and was created by [Jared Palmer](https://twitter.com/jaredpalmer) — it was [acquired by Vercel a year ago](https://vercel.com/blog/vercel-acquires-turborepo). 

Turborepo is fast, easy to use and configure, and serves as a lightweight layer that can easily be added or replaced. It's built on top of workspaces, a feature that comes with all major package managers. We'll cover workspaces in more detail in the next section. 

Once Turborepo has been installed and configured in your monorepo, it will understand how your projects depend on each other and maximize running speed for your scripts and tasks. 

Turborepo doesn't do the same work twice; it has a caching system that allows for the skipping of work that has already been done before. The cache also keeps track of multiple versions, so if you roll back to a previous version it can reuse earlier versions of the “files” cache. 

The [Turborepo documentation](https://turborepo.org/docs) is a great resource to learn more. The [official Turborepo handbook](https://turborepo.org/docs/handbook) also covers important aspects of monorepos in general and related topics, like migrating to a monorepo, development workflows, code sharing, linting, testing, publishing, and deployment.

## Structuring the base monorepo <a name="structuring-the-basemonorepo">

### Workspaces with pnpm

Workspaces are the base building blocks for a monorepo. All major package managers have built-in support for workspaces, including [npm](https://docs.npmjs.com/cli/v8/using-npm/workspaces), [yarn](https://classic.yarnpkg.com/lang/en/docs/workspaces), and [pnpm](https://pnpm.io/workspaces). 

Workspaces provide support for managing multiple projects in a single repository. Each project is contained in a workspace with its own `package.json`, source code, and configuration files. 

There's also a `package.json` at the root level of the monorepo and a lock file. The lock file keeps a reference of all packages installed across all workspaces, so you only need to run `pnpm install` or `npm install` once to install all workspace dependencies. 

We'll be using pnpm, not only for its efficiency, speed, and disk space usage, but because it also has good support for managing workspaces and it's recommended by the Turborepo team. 

You can check out this [article](https://blog.logrocket.com/managing-full-stack-monorepo-pnpm) to learn more about managing a full-stack monorepo with pnpm. 

If you don't have pnpm installed, check out their [installation guide](https://pnpm.io/installation). You can also use npm or yarn workspaces instead of pnpm workspaces if you prefer.

### Structure overview

We'll start with the general high-level structure. 

First, we'll place `api`, `web`, and `types` inside a `packages` directory in the monorepo root. At the root level, we also have a `package.json` and a `pnpm-workspace.yaml` configuration file for pnpm to specify which packages are workspaces, as shown here:

```shell
.
├── packages
│   ├── api/
│   ├── types/
│   └── web/
├── package.json
└── pnpm-workspace.yaml
```

We can quickly create the `packages` directory and its sub-directories with the following `mkdir` command:

```shell
mkdir -p packages/{api,types,web}
```

We will then run `pnpm init` in the monorepo root and in the three packages:

```shell
pnpm init

cd packages/api; pnpm init
cd ../../packages/types; pnpm init
cd ../../packages/web; pnpm init

cd ../..
```

Notice we used `../..` to go back two directories after each `cd` command, before finally going back to the monorepo root with the `cd ../..` command. 

We want any direct child directory inside the `packages` directory to be a workspace, but pnpm and other package managers don't recognize workspaces until we explicitly define them. 

Configuring workspaces implies that we specify workspaces either by listing each workspace individually, or with a pattern to match multiple directories or workspaces at once. This configuration is written inside the root level `pnpm-workspace.yaml` file. 

We'll use a glob pattern to match all the `packages` directly to the children directories. Here's the configuration:

```yaml
# pnpm-workspace.yaml

packages:
  - 'packages/*'
```

For performance reasons, it's better to avoid nested glob matching like `packages/**`, as it will match not only the direct children, but all the directories inside the `packages` directory. 

We chose to use the name `packages` as the directory that includes our workspaces, but it can be named differently; `apps` and `libs` are my personal preferences (inspired by Nx). 

You can also have multiple workspace directories after adding them to [`pnpm-workspace.yaml`](https://pnpm.io/pnpm-workspace_yaml). 

In the following sections, we'll set up a base project for each workspace and install their dependencies.

## Shared types package setup

We'll start by setting up the types package at `packages/types`. 

`typescript` is the only dependency we need for this workspace. Here's the command to install it as a dev dependency:

```shell
pnpm add --save-dev typescript --filter types
```

The `package.json` should look like this:

```json
// packages/types/package.json

{
  "name": "types",
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "scripts": {
    "type-check": "tsc"
  },
  "devDependencies": {
    "typescript": "^4.8.4"
  }
}
```

We'll now add the configuration file for TypeScript:

```json
// packages/types/tsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "target": "es2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "include": ["./src"]
}
```

Now that everything is ready, let's add and export the type that we'll use for both `api` and `web`.

```typescript
// packages/types/src/index.ts

export type Workspace = {
  name: string
  version: string
}
```

The shared `types` workspace, or any shared workspace for that matter, should be installed in the other workspaces using it. The shared workspace will be listed alongside the other dependencies or dev dependencies inside the consuming workspace’s `package.json`. 

pnpm has a dedicated protocol (`workspace:<version>`) to resolve a local workspace with linking. You might also want to change the workspace `<version>` to `*` to ensure you always have the latest workspace version. 

We can use the following command to install the `types` workspace:

```shell
pnpm add --save-dev types@workspace --filter <workspace>
```

> **N.B.**, the package name used to install and reference the `types` workspace should be named exactly as the defined `name` field inside the `types` workspace `package.json`

## Backend setup (Express, TypeScript, `esbuild`, `tsx`) <a name="backend-setup-express-typescript-es-build-tsx">

We'll now build a simple backend API using Node.js and Express at `packages/api`. 

Here are our dependencies and dev dependencies:

```shell
pnpm add express cors --filter api
pnpm add --save-dev typescript esbuild tsx @types/{express,cors} --filter api
pnpm add --save-dev types@workspace --filter api
```

The `package.json` should look something like this:

```shell
// packages/api/package.json

{
  "name": "api",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --external:express --external:cors",
    "start": "node dist/index.js",
    "type-check": "tsc"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.18.1"
  },
  "devDependencies": {
    "@types/cors": "^2.8.12",
    "@types/express": "^4.17.14",
    "esbuild": "^0.15.11",
    "tsx": "^3.10.1",
    "types": "workspace:*",
    "typescript": "^4.8.4"
  }
}
```

We'll use the exact same `tsconfig.json` from the `types` workspace. 

Finally, we'll add the app entry and expose one endpoint:

```typescript
// packages/api/src/index.ts

import cors from 'cors'
import express from 'express'

import { Workspace } from 'types'

const app = express()
const port = 5000

app.use(cors({ origin: 'http://localhost:3000' }))

app.get('/workspaces', (_, response) => {
  const workspaces: Workspace[] = [
    { name: 'api', version: '1.0.0' },
    { name: 'types', version: '1.0.0' },
    { name: 'web', version: '1.0.0' },
  ]
  response.json({ data: workspaces })
})

app.listen(port, () => console.log(`Listening on http://localhost:${port}`))
```

## Frontend (React, TypeScript, Vite) setup

This is the last workspace we'll add and it will be located in `packages/web`. These are the dependencies to install:

```shell
pnpm add react react-dom --filter web
pnpm add --save-dev typescript vite @vitejs/plugin-react @types/{react,react-dom} --filter web
pnpm add --save-dev types@workspace --filter web
```

The `package.json` should look something like this:

```json
// packages/web/package.json

{
  "name": "web",
  "scripts": {
    "dev": "vite dev --port 3000",
    "build": "vite build",
    "start": "vite preview",
    "type-check": "tsc"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.0.21",
    "@types/react-dom": "^18.0.6",
    "@vitejs/plugin-react": "^2.1.0",
    "types": "workspace:*",
    "typescript": "^4.8.4",
    "vite": "^3.1.6"
  }
}
```

Again, we'll use the same `tsconfig.json` file we used for `types` and `api`, adding only one line at `compilerOptions` for Vite's client types:

```json
// packages/web/tsconfig.json

{
  "compilerOptions": {
    // ...
    "types": ["vite/client"]
  }
  // ...
}
```

Now, let’s add the `vite.config.ts` and the entry `index.html`:

```typescript
// packages/web/vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
})
```

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Building a fullstack TypeScript project with Turborepo</title>
  </head>

  <body>
    <div id="app"></div>
    <script type="module" src="/src/index.tsx"></script>
  </body>
</html>
```

And finally, here's our entry for the React application at `src/index.tsx`:

```javascript
// packages/web/src/index.tsx

import { StrictMode, useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'

import { Workspace } from 'types'

const App = () => {
  const [data, setData] = useState<Workspace[]>([])

  useEffect(() => {
    fetch('http://localhost:5000/workspaces')
      .then((response) => response.json())
      .then(({ data }) => setData(data))
  }, [])

  return (
    <StrictMode>
      <h1>Building a fullstack TypeScript project with Turborepo</h1>
      <h2>Workspaces</h2>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </StrictMode>
  )
}

const app = document.querySelector('#app')
if (app) createRoot(app).render(<App />)
```

## Adding Turborepo <a name="adding-turbo-repo">

If your monorepo is simple, with only a few workspaces, managing them with pnpm workspaces can be totally sufficient. 

However, with bigger projects, we'll need to have a more efficient monorepo tool to manage their complexity and scale. Turborepo can improve your workspaces by speeding up your linting, testing, and building of pipelines without changing the structure of your monorepo. 

The speed gains are mainly because of Turborepo's [caching system](https://turbo.build/repo/docs/core-concepts/caching). After running a task, it will not run again until the workspace itself or a dependent workspace has changed. 

In addition, Turborepo can multitask; it schedules tasks to maximize the speed of executing them.

> **N.B.**, you can read more about running tasks in the Turborepo [core concepts guide](https://turborepo.org/docs/core-concepts/running-tasks))

Here’s an example from the Turborepo docs comparing running workspace tasks with the package manager directly versus running tasks using Turborepo (image source [here](https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks)): ![Turborepo Running Workspace Tasks](https://blog.logrocket.com/wp-content/uploads/2022/11/turborepo-running-workspace-tasks.png) 

Running the same tasks with Turborepo will result in faster and more optimized execution: ![Turborepo Running Tasks Comparison With TR](https://blog.logrocket.com/wp-content/uploads/2022/11/turborepo-running-tasks-comparison-with-tr.png)

### Installation and configuration

As mentioned earlier, we don't need to modify our workspace setups to use Turborepo. We'll just need to do two things to get it to work with our existing monorepo. 

Let's first install the `turbo` package at the monorepo root:

```shell
pnpm add --save-dev --workspace-root turbo
```

And let’s also add the `.turbo` directory to the `.gitignore` file, along with the task’s artifacts, files, and directories we want to cache — like the `dist` directory in our case. The `.gitignore` file should look something like this:

```shell
.turbo
node_modules
dist
```

> **N.B.**, make sure to have Git initialized in your monorepo root by running `git init`, if you haven’t already, as Turborepo uses Git with file hashing for caching

Now, we can configure our Turborepo pipelines at `turbo.json`. Pipelines allow us to declare which tasks depend on each other inside our monorepo. The pipelines infer the tasks’ dependency graph to properly schedule, execute, and cache the task outputs. 

Each pipeline direct key is a runnable task via `turbo run <task>`. If we don't include a task name inside the workspace's `package.json` `scripts`, the task will be ignored for the corresponding workspace. 

These are the tasks that we want to define for our monorepo: `dev`, `type-check`, and `build`. 

Let's start defining each task with its options:

```json
// turbo.json

{
  "pipeline": {
    "dev": {
      "cache": false
    },
    "type-check": {
      "outputs": []
    },
    "build": {
      "dependsOn": ["type-check"],
      "outputs": ["dist/**"]
    }
  }
}
```

`cache` is an enabled option by default; we've disabled it for the `dev` task. The `output` option is an array. If it's empty, it will cache the task logs; otherwise, it will cache the task-specified outputs. 

We use `dependsOn` to run the `type-check` task for each workspace before running its `build` task. 

`cache` and `outputs` are straightforward to use, but `dependsOn` has multiple cases. You can learn more about configuration options at the reference [here](https://turbo.build/repo/docs/reference/configuration). 

Here's an overview of the file structure so far after adding Turborepo:

```shell
.
├── packages
│   ├── api
│   │   ├── package.json
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── types
│   │   ├── package.json
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   └── web
│       ├── index.html
│       ├── package.json
│       ├── src
│       │   └── index.tsx
│       ├── tsconfig.json
│       └── vite.config.ts
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json
```

## What's next?

Monorepos facilitate the managing and scaling of complex applications. Using Turborepo on top of workspaces is a great option in a lot of use cases. 

We’ve only scratched the surface of what we can do with Turborepo. You can find more examples in the [Turborepo examples directory on GitHub](https://github.com/vercel/turborepo/tree/main/examples). [Skill Recordings on GitHub](https://github.com/skillrecordings/products) is also another great resource that has been around since Turborepo was first released. 

We highly recommend that you look at [Turborepo core concepts](https://turborepo.org/docs/core-concepts/why-turborepo) and [the new handbook](https://turborepo.org/docs/handbook). There are also a couple of informative YouTube videos about Turborepo on [Vercel's channel](https://www.youtube.com/c/VercelHQ) which you may find useful. 

Feel free to leave a comment below and share what you think about Turborepo, or if you have any questions. Share this post if you find it useful and stay tuned for upcoming posts!

---
## [LogRocket](https://lp.logrocket.com/blg/signup): Full visibility into your web and mobile apps

[![LogRocket signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://lp.logrocket.com/blg/signup)

[LogRocket](https://lp.logrocket.com/blg/signup) is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

[Try it for free.](https://lp.logrocket.com/blg/typescript-signup)