Boost Your Development With A Pnpm, Turborepo, And TypeScript Monorepo

by SLV Team 71 views
Boost Your Development with a pnpm, Turborepo, and TypeScript Monorepo

Hey guys! Ready to dive into the world of monorepos and supercharge your development workflow? This guide will walk you through setting up a slick monorepo using pnpm, Turborepo, and TypeScript. We'll cover everything from initializing the project to setting up shared configurations and scripts, ensuring a clean and efficient development environment. By the end, you'll have a solid foundation for building scalable and maintainable applications. Let's get started!

Understanding the Core Technologies

Before we jump into the setup, let's quickly go over the key players in our monorepo setup: pnpm, Turborepo, and TypeScript. Understanding these tools will help you appreciate the benefits of this setup.

pnpm: The Efficient Package Manager

pnpm (performant npm) is a fast and disk space-efficient package manager. Unlike npm or yarn, pnpm uses hard links and symbolic links to store project dependencies. This means that dependencies are stored in a central location on your disk, and projects link to these dependencies. This approach drastically reduces the amount of disk space used, especially in a monorepo where many packages might share the same dependencies. Additionally, pnpm boasts superior performance compared to npm and yarn, leading to faster installation times and a more responsive development experience. It's designed to be a drop-in replacement for npm and yarn, so the transition is usually smooth.

Turborepo: The Build Accelerator

Turborepo is a high-performance build system designed for monorepos. It's built by Vercel. Turborepo intelligently caches build outputs and only rebuilds what has changed, resulting in significant time savings during development and CI/CD pipelines. This means that when you make a change in one package, Turborepo only rebuilds that package and any packages that depend on it. Turborepo also offers features like task scheduling and remote caching, further optimizing build processes. It's the secret weapon for managing complex builds and keeping your development cycles fast.

TypeScript: The Type-Safe Superhero

TypeScript is a superset of JavaScript that adds static typing. TypeScript helps catch errors early in the development process, making your code more reliable and easier to maintain. Type annotations and interfaces provide clarity and improve code readability, especially in larger projects. TypeScript also offers excellent tooling support, including autocompletion, refactoring, and error checking, which makes it an indispensable part of modern web development. TypeScript helps prevent those sneaky runtime errors that can be a real pain to debug.

Setting Up Your Monorepo

Now, let's get our hands dirty and create our monorepo! We'll walk through the steps, ensuring you have a working setup. This is where the magic happens, so pay close attention!

Step 1: Initialize the Monorepo

First, let's create a new directory for our monorepo and initialize it with pnpm. Open your terminal and run the following commands:

mkdir my-monorepo
cd my-monorepo
pnpm init -y

This creates a new directory, navigates into it, and initializes a package.json file. The -y flag accepts all default values, so you don't need to answer any prompts.

Step 2: Install Dependencies

Next, install the necessary dependencies: Turborepo and TypeScript. We'll install these as devDependencies because they are primarily used during development and build processes.

pnpm add -D turbo typescript @tsconfig/strictest

This command adds the packages we need. turbo is the Turborepo CLI, typescript is the TypeScript compiler, and @tsconfig/strictest provides a strict TypeScript configuration.

Step 3: Create the Directory Structure

Now, let's set up the basic directory structure for our monorepo. We'll create the apps, packages, and other necessary directories. This structure will keep things organized and make it easier to manage our projects.

mkdir apps
mkdir packages

cd apps
mkdir client server
cd ..

cd packages
mkdir types
cd ..

This creates the following structure:

my-monorepo/
├── apps/
│   ├── client/
│   └── server/
├── packages/
│   └── types/
└── package.json

Step 4: Configure TypeScript

Let's configure TypeScript for our project. We'll start by creating a tsconfig.base.json file in the root of the monorepo. This file will contain our base configuration, which we'll extend in our individual packages.

touch tsconfig.base.json

Add the following content to tsconfig.base.json:

{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "inlineSources": true,
    "module": "esnext",
    "moduleResolution": "node",
    "noEmitOnError": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "paths": {
      "@types/*": ["packages/types/src/*"]
    },
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "target": "es5"
  },
  "exclude": ["node_modules"]
}

This configuration sets up strict type checking, enables module resolution, and defines a path alias (@types/*) for our shared types package. We also need to create a tsconfig.json file inside the packages/types directory:

cd packages/types
pnpm init -y
touch tsconfig.json

Then, add the following to packages/types/tsconfig.json:

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["./src/**/*"]
}

This extends our base configuration and sets the output directory and root directory for the types package. Let's create the src directory and an example index.ts file inside packages/types. This will contain shared types that other packages can use.

mkdir src
touch src/index.ts

Add the following to packages/types/src/index.ts:

export interface ExampleType {
  id: number;
  name: string;
}

This simple interface will be our example shared type.

Step 5: Configure .gitignore and .editorconfig

To keep our repository clean and consistent, we'll add a shared .gitignore file and an .editorconfig file to the root of our monorepo. This will help us avoid committing unnecessary files and maintain consistent code formatting.

Create a .gitignore file in the root:

touch .gitignore

Add the following content to .gitignore:

node_modules
dist
.turbo

This tells Git to ignore node_modules, dist directories (where compiled code goes), and the Turborepo cache. This prevents us from committing large, unnecessary files to our repository.

Create an .editorconfig file in the root:

touch .editorconfig

Add the following content to .editorconfig:

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

This configuration ensures consistent code formatting across the project, such as using spaces for indentation and setting the end-of-line character to LF.

Step 6: Create Package.json Files for Apps

Let's get the ball rolling and create package.json files for our client and server apps. This will allow us to define their dependencies and scripts.

First, create the client and server package.json files.

cd ../../apps/client
pnpm init -y
cd ../server
pnpm init -y

Next, inside the apps/client/package.json, let's add these scripts:

{
  "name": "client",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "lint": "tsc --noEmit"
  },
  "devDependencies": {
    "typescript": "^5.0.0"
  }
}

And now let's create the apps/server/package.json file. Add these scripts:

{
  "name": "server",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "lint": "tsc --noEmit"
  },
  "devDependencies": {
    "typescript": "^5.0.0"
  }
}

Step 7: Configure Turborepo

Now, let's configure Turborepo to manage our build processes. We'll create a turbo.json file in the root of our monorepo to define the tasks and dependencies.

touch turbo.json

Add the following content to turbo.json:

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".dist"]
    },
    "dev": {
      "cache": false,
      "dependsOn": ["^build"]
    },
    "lint": {}
  }
}

This configuration defines a pipeline for our build, dev, and lint tasks. The build task depends on the build tasks of the dependencies and outputs the .dist directory, while dev doesn't use the cache. This sets up the basic task management structure for Turborepo.

Step 8: Add Root Scripts

Let's add some root scripts to our package.json file to make it easier to run common tasks. We'll add dev, build, lint, and test scripts.

Open the root package.json file and add the following scripts:

{
  "name": "my-monorepo",
  "private": true,
  "version": "0.0.0",
  "workspaces": [
    "apps/*",
    "packages/*"
  ],
  "scripts": {
    "dev": "turbo run dev --parallel",
    "build": "turbo run build",
    "lint": "turbo run lint",
    "test": ""
  },
  "devDependencies": {
    "turbo": "latest",
    "typescript": "^5.0.0"
  },
  "packageManager": "pnpm@8.6.13"
}

These scripts use turbo to run the corresponding tasks in our packages. The --parallel flag for dev will run the dev scripts of all packages in parallel. The test script is left blank for now, but you can add testing commands here later.

Step 9: Install Dependencies and Build

Now, it's time to install all dependencies and build our project. This is where we make sure everything is wired up correctly and that we don't have any errors.

Run the following command in your terminal:

pnpm install
pnpm run build

pnpm install will install all the dependencies for our packages, and pnpm run build will run the build tasks defined in turbo.json. If everything is set up correctly, the build should complete successfully without any errors.

Testing Your Setup

Let's test our setup to make sure everything is working as expected. This will give us confidence that our monorepo is correctly configured and ready for development. This is where we verify that our setup compiles with no errors. Let's make sure the types package works!

Step 1: Add a Client Index File

Let's add the basic client file, to make sure it can recognize the interface from the types package.

Create a file apps/client/src/index.ts:

import { ExampleType } from '@types/index';

function greet(example: ExampleType) {
  console.log(`Hello, ${example.name}!`);
}

const example: ExampleType = {
  id: 1,
  name: 'World',
};

greet(example);

Create a file apps/server/src/index.ts:

import { ExampleType } from '@types/index';

function greet(example: ExampleType) {
  console.log(`Hello, ${example.name}!`);
}

const example: ExampleType = {
  id: 1,
  name: 'World',
};

greet(example);

This imports the ExampleType we defined in packages/types/src/index.ts and uses it. The fact that it compiles successfully means our path aliases and shared types are working.

Step 2: Run the Build Command

Let's run the build command again to check for errors.

pnpm run build

If the build completes without errors, it confirms that your TypeScript configuration and path aliases are correctly set up. The build process should generate .dist directories for each package. If there are no errors, then all is set up properly.

Conclusion: You Did It!

Congrats, guys! You've successfully set up a pnpm, Turborepo, and TypeScript monorepo. This structure provides a solid foundation for building scalable, maintainable, and efficient applications. From efficient package management with pnpm to accelerated builds with Turborepo and the type safety of TypeScript, you're well-equipped to take on your next project.

Remember to explore further: consider adding more packages, setting up CI/CD, and using Turborepo's caching features to optimize your development workflow even further. Happy coding! Don't be afraid to experiment and customize your monorepo to fit your specific needs. Keep it fun, and enjoy the streamlined development experience!