Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?

Building a full-stack TypeScript application with Turborepo

Written by Omar Elhawary✏️

Whether or not you are constructing a full-stack software or an software composed of a number of frontend and backend tasks, you will in all probability must share elements throughout tasks to various extents.

It might be sorts, utilities, validation schemas, parts, design programs, growth instruments, or configurations. Monorepos assist devs handle all these elements in a single repository.

On this article, we’ll present an outline of what monorepos are and what the advantages are of utilizing Turborepo. We’ll then construct a easy full-stack software utilizing Turborepo with React and Node.js utilizing pnpm workspaces and exhibit how the method might be improved by utilizing Turborepo.



What’s a monorepo?

A monorepo is a single repository that incorporates a number of functions and/or libraries. Monorepos facilitate undertaking administration, code sharing, cross-repo modifications with instantaneous type-checking validation, and extra.

Turborepo is among the finest monorepo instruments within the JavaScript/TypeScript ecosystem.

It is quick, simple to configure and use, impartial from the applying applied sciences, and might be adopted incrementally. It has a small studying curve and a low barrier to entry — whether or not you are simply beginning out with monorepos or are skilled and trying to attempt completely different instruments within the ecosystem.

This is a illustration of the construction of a monorepo and a polyrepo (supply might be discovered here):



Polyrepos

As an instance we’re constructing a full-stack software; each the frontend and the backend are two separate tasks, every of them positioned in a distinct repository — this can be a polyrepo.

If we have to share sorts or utilities between the frontend and the backend and we do not need to duplicate them on each tasks, we now have to create a 3rd repository and eat them as an exterior package deal for each tasks.

Every time we modify the shared package deal, we now have to construct and publish a brand new model. Then, all tasks utilizing this package deal ought to replace to the most recent model.

Along with the overhead of versioning and publishing, these a number of elements can fairly simply turn into out of sync with a excessive risk of frequent breakages.

There are different shortcomings to polyrepos relying in your undertaking, and utilizing a monorepo is another that addresses a few of these points.



Optimizing monorepos

Utilizing monorepos with out the precise tooling could make functions harder to handle than utilizing polyrepos. To have an optimized monorepo, you will want a caching system together with optimized process execution to save lots of growth and deployment time.

There are lots of instruments like Lerna, Nx, Turborepo, Moon, Rush, and Bazel, to call a number of. In the present day, we’ll be utilizing Turborepo, because it’s light-weight, versatile, and simple to make use of.

You possibly can be taught extra about monorepos, when and why to make use of them, and a comparability between varied instruments at monorepo.tools.



What’s Turborepo?

Turborepo is a well-liked monorepo device within the JavaScript/TypeScript ecosystem. It is written in Go and was created by Jared Palmer — it was acquired by Vercel a year ago.

Turborepo is quick, simple to make use of and configure, and serves as a light-weight layer that may simply be added or changed. It is constructed on high of workspaces, a function that comes with all main package deal managers. We’ll cowl workspaces in additional element within the subsequent part.

As soon as Turborepo has been put in and configured in your monorepo, it’s going to perceive how your tasks rely on one another and maximize working velocity on your scripts and duties.

Turborepo does not do the identical work twice; it has a caching system that permits for the skipping of labor that has already been achieved earlier than. The cache additionally retains monitor of a number of variations, so in case you roll again to a earlier model it could actually reuse earlier variations of the “recordsdata” cache.

The Turborepo documentation is a superb useful resource to be taught extra. The official Turborepo handbook additionally covers vital points of monorepos usually and associated matters, like migrating to a monorepo, growth workflows, code sharing, linting, testing, publishing, and deployment.



Structuring the bottom monorepo



Workspaces with pnpm

Workspaces are the bottom constructing blocks for a monorepo. All main package deal managers have built-in assist for workspaces, together with npm, yarn, and pnpm.

Workspaces present assist for managing a number of tasks in a single repository. Every undertaking is contained in a workspace with its personal package deal.json, supply code, and configuration recordsdata.

There’s additionally a package deal.json on the root stage of the monorepo and a lock file. The lock file retains a reference of all packages put in throughout all workspaces, so that you solely must run pnpm set up or npm set up as soon as to put in all workspace dependencies.

We’ll be utilizing pnpm, not just for its effectivity, velocity, and disk area utilization, however as a result of it additionally has good assist for managing workspaces and it is advisable by the Turborepo staff.

You possibly can try this article to be taught extra about managing a full-stack monorepo with pnpm.

If you do not have pnpm put in, try their installation guide. You too can use npm or yarn workspaces as a substitute of pnpm workspaces in case you choose.



Construction overview

We’ll begin with the final high-level construction.

First, we’ll place api, net, and sorts inside a packages listing within the monorepo root. On the root stage, we even have a package deal.json and a pnpm-workspace.yaml configuration file for pnpm to specify which packages are workspaces, as proven right here:

.
├── packages
│   ├── api/
│   ├── sorts/
│   └── net/
├── package deal.json
└── pnpm-workspace.yaml
Enter fullscreen mode

Exit fullscreen mode

We will rapidly create the packages listing and its sub-directories with the next mkdir command:

mkdir -p packages/{api,sorts,net}
Enter fullscreen mode

Exit fullscreen mode

We are going to then run pnpm init within the monorepo root and within the three packages:

pnpm init

cd packages/api; pnpm init
cd ../../packages/sorts; pnpm init
cd ../../packages/net; pnpm init

cd ../..
Enter fullscreen mode

Exit fullscreen mode

Discover we used ../.. to return two directories after every cd command, earlier than lastly going again to the monorepo root with the cd ../.. command.

We would like any direct baby listing contained in the packages listing to be a workspace, however pnpm and different package deal managers do not acknowledge workspaces till we explicitly outline them.

Configuring workspaces implies that we specify workspaces both by itemizing every workspace individually, or with a sample to match a number of directories or workspaces directly. This configuration is written inside the foundation stage pnpm-workspace.yaml file.

We’ll use a glob sample to match all of the packages on to the kids directories. This is the configuration:

# pnpm-workspace.yaml

packages:
  - 'packages/*'
Enter fullscreen mode

Exit fullscreen mode

For efficiency causes, it is higher to keep away from nested glob matching like packages/**, as it’s going to match not solely the direct youngsters, however all of the directories contained in the packages listing.

We selected to make use of the title packages because the listing that features our workspaces, however it may be named in a different way; apps and libs are my private preferences (impressed by Nx).

You too can have a number of workspace directories after including them to pnpm-workspace.yaml.

Within the following sections, we’ll arrange a base undertaking for every workspace and set up their dependencies.



Shared sorts package deal setup

We’ll begin by establishing the kinds package deal at packages/sorts.

typescript is the one dependency we want for this workspace. This is the command to put in it as a dev dependency:

pnpm add --save-dev typescript --filter sorts
Enter fullscreen mode

Exit fullscreen mode

The package deal.json ought to appear like this:

// packages/sorts/package deal.json

{
  "title": "sorts",
  "most important": "./src/index.ts",
  "sorts": "./src/index.ts",
  "scripts": {
    "type-check": "tsc"
  },
  "devDependencies": {
    "typescript": "^4.8.4"
  }
}
Enter fullscreen mode

Exit fullscreen mode

We’ll now add the configuration file for TypeScript:

// packages/sorts/tsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "goal": "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": "protect"
  },
  "embrace": ["./src"]
}
Enter fullscreen mode

Exit fullscreen mode

Now that every thing is prepared, let’s add and export the sort that we’ll use for each api and net.

// packages/sorts/src/index.ts

export kind Workspace = {
  title: string
  model: string
}
Enter fullscreen mode

Exit fullscreen mode

The shared sorts workspace, or any shared workspace for that matter, must be put in within the different workspaces utilizing it. The shared workspace might be listed alongside the opposite dependencies or dev dependencies contained in the consuming workspace’s package deal.json.

pnpm has a devoted protocol (workspace:<model>) to resolve a neighborhood workspace with linking. You may also need to change the workspace <model> to * to make sure you at all times have the newest workspace model.

We will use the next command to put in the sorts workspace:

pnpm add --save-dev sorts@workspace --filter <workspace>
Enter fullscreen mode

Exit fullscreen mode

N.B., the package deal title used to put in and reference the sorts workspace must be named precisely because the outlined title area contained in the sorts workspace package deal.json



Backend setup (Specific, TypeScript, esbuild, tsx)

We’ll now construct a easy backend API utilizing Node.js and Specific at packages/api.

Listed here are our dependencies and dev dependencies:

pnpm add specific cors --filter api
pnpm add --save-dev typescript esbuild tsx @sorts/{specific,cors} --filter api
pnpm add --save-dev sorts@workspace --filter api
Enter fullscreen mode

Exit fullscreen mode

The package deal.json ought to look one thing like this:

// packages/api/package deal.json

{
  "title": "api",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "construct": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js --external:specific --external:cors",
    "begin": "node dist/index.js",
    "type-check": "tsc"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "specific": "^4.18.1"
  },
  "devDependencies": {
    "@sorts/cors": "^2.8.12",
    "@sorts/specific": "^4.17.14",
    "esbuild": "^0.15.11",
    "tsx": "^3.10.1",
    "sorts": "workspace:*",
    "typescript": "^4.8.4"
  }
}
Enter fullscreen mode

Exit fullscreen mode

We’ll use the very same tsconfig.json from the sorts workspace.

Lastly, we’ll add the app entry and expose one endpoint:

// packages/api/src/index.ts

import cors from 'cors'
import specific from 'specific'

import { Workspace } from 'sorts'

const app = specific()
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({ knowledge: workspaces })
})

app.pay attention(port, () => console.log(`Listening on http://localhost:${port}`))
Enter fullscreen mode

Exit fullscreen mode



Frontend (React, TypeScript, Vite) setup

That is the final workspace we’ll add and will probably be positioned in packages/net. These are the dependencies to put in:

pnpm add react react-dom --filter net
pnpm add --save-dev typescript vite @vitejs/plugin-react @sorts/{react,react-dom} --filter net
pnpm add --save-dev sorts@workspace --filter net
Enter fullscreen mode

Exit fullscreen mode

The package deal.json ought to look one thing like this:

// packages/net/package deal.json

{
  "title": "net",
  "scripts": {
    "dev": "vite dev --port 3000",
    "construct": "vite construct",
    "begin": "vite preview",
    "type-check": "tsc"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@sorts/react": "^18.0.21",
    "@sorts/react-dom": "^18.0.6",
    "@vitejs/plugin-react": "^2.1.0",
    "sorts": "workspace:*",
    "typescript": "^4.8.4",
    "vite": "^3.1.6"
  }
}
Enter fullscreen mode

Exit fullscreen mode

Once more, we’ll use the identical tsconfig.json file we used for sorts and api, including just one line at compilerOptions for Vite’s consumer sorts:

// packages/net/tsconfig.json

{
  "compilerOptions": {
    // ...
    "sorts": ["vite/client"]
  }
  // ...
}
Enter fullscreen mode

Exit fullscreen mode

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

// packages/net/vite.config.ts

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

export default defineConfig({
  plugins: [react()],
})
Enter fullscreen mode

Exit fullscreen mode

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

  <physique>
    <div id="app"></div>
    <script kind="module" src="/src/index.tsx"></script>
  </physique>
</html>
Enter fullscreen mode

Exit fullscreen mode

And eventually, this is our entry for the React software at src/index.tsx:

// packages/net/src/index.tsx

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

import { Workspace } from 'sorts'

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

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

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

const app = doc.querySelector('#app')
if (app) createRoot(app).render(<App />)
Enter fullscreen mode

Exit fullscreen mode



Including Turborepo

In case your monorepo is easy, with just a few workspaces, managing them with pnpm workspaces might be completely enough.

Nonetheless, with larger tasks, we’ll must have a extra environment friendly monorepo device to handle their complexity and scale. Turborepo can enhance your workspaces by dashing up your linting, testing, and constructing of pipelines with out altering the construction of your monorepo.

The velocity positive aspects are primarily due to Turborepo’s caching system. After working a process, it is not going to run once more till the workspace itself or a dependent workspace has modified.

As well as, Turborepo can multitask; it schedules duties to maximise the velocity of executing them.

N.B., you possibly can learn extra about working duties within the Turborepo core concepts guide)

Right here’s an instance from the Turborepo docs evaluating working workspace duties with the package deal supervisor straight versus working duties utilizing Turborepo (picture supply here): Turborepo Running Workspace Tasks

Working the identical duties with Turborepo will lead to sooner and extra optimized execution: Turborepo Running Tasks Comparison With TR



Set up and configuration

As talked about earlier, we need not modify our workspace setups to make use of Turborepo. We’ll simply must do two issues to get it to work with our current monorepo.

Let’s first set up the turbo package deal on the monorepo root:

pnpm add --save-dev --workspace-root turbo
Enter fullscreen mode

Exit fullscreen mode

And let’s additionally add the .turbo listing to the .gitignore file, together with the duty’s artifacts, recordsdata, and directories we need to cache — just like the dist listing in our case. The .gitignore file ought to look one thing like this:

.turbo
node_modules
dist
Enter fullscreen mode

Exit fullscreen mode

N.B., be certain that to have Git initialized in your monorepo root by working git init, in case you haven’t already, as Turborepo makes use of Git with file hashing for caching

Now, we will configure our Turborepo pipelines at turbo.json. Pipelines enable us to declare which duties rely on one another inside our monorepo. The pipelines infer the duties’ dependency graph to correctly schedule, execute, and cache the duty outputs.

Every pipeline direct secret’s a runnable process by way of turbo run <process>. If we do not embrace a process title contained in the workspace’s package deal.json scripts, the duty might be ignored for the corresponding workspace.

These are the duties that we need to outline for our monorepo: dev, type-check, and construct.

Let’s begin defining every process with its choices:

// turbo.json

{
  "pipeline": {
    "dev": {
      "cache": false
    },
    "type-check": {
      "outputs": []
    },
    "construct": {
      "dependsOn": ["type-check"],
      "outputs": ["dist/**"]
    }
  }
}
Enter fullscreen mode

Exit fullscreen mode

cache is an enabled possibility by default; we have disabled it for the dev process. The output possibility is an array. If it is empty, it’s going to cache the duty logs; in any other case, it’s going to cache the task-specified outputs.

We use dependsOn to run the type-check process for every workspace earlier than working its construct process.

cache and outputs are simple to make use of, however dependsOn has a number of instances. You possibly can be taught extra about configuration choices on the reference here.

This is an outline of the file construction to this point after including Turborepo:

.
├── packages
│   ├── api
│   │   ├── package deal.json
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── sorts
│   │   ├── package deal.json
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   └── net
│       ├── index.html
│       ├── package deal.json
│       ├── src
│       │   └── index.tsx
│       ├── tsconfig.json
│       └── vite.config.ts
├── package deal.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json
Enter fullscreen mode

Exit fullscreen mode



What’s subsequent?

Monorepos facilitate the managing and scaling of advanced functions. Utilizing Turborepo on high of workspaces is a superb possibility in numerous use instances.

We’ve solely scratched the floor of what we will do with Turborepo. You could find extra examples within the Turborepo examples directory on GitHub. Skill Recordings on GitHub can be one other nice useful resource that has been round since Turborepo was first launched.

We extremely suggest that you just have a look at Turborepo core concepts and the new handbook. There are additionally a few informative YouTube movies about Turborepo on Vercel’s channel which you’ll discover helpful.

Be at liberty to depart a remark under and share what you consider Turborepo, or when you’ve got any questions. Share this submit in case you discover it helpful and keep tuned for upcoming posts!




LogRocket: Full visibility into your net and cellular apps

LogRocket signup

LogRocket is a frontend software monitoring answer that permits you to replay issues as in the event that they occurred in your individual browser. As a substitute of guessing why errors occur, or asking customers for screenshots and log dumps, LogRocket enables you to replay the session to rapidly perceive what went flawed. It really works completely with any app, no matter framework, and has plugins to log further context from Redux, Vuex, and @ngrx/retailer.

Along with logging Redux actions and state, LogRocket information console logs, JavaScript errors, stacktraces, community requests/responses with headers + our bodies, browser metadata, and customized logs. It additionally devices the DOM to document the HTML and CSS on the web page, recreating pixel-perfect movies of even probably the most advanced single-page and cellular apps.

Try it for free.



Add a Comment

Your email address will not be published. Required fields are marked *

Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?