AboutProjectsNotion

Tuesday 19 / 08 / 2025

タイラーRETOURNER

Turborepo Example with NestJS and Next.js

Table of Contents

  • Monorepo Architecture
  • What is tRPC and Why Do We Use It?
  • Environment Setup
  • Backend Development Workflow
  • Frontend Development Workflow
  • Database Management
  • tRPC Integration
  • Useful Commands
  • Best Practices
  • Monorepo Architecture

    This project uses Turborepo to manage a monorepo with the following applications and packages:

    Applications (apps/)

  • apps/backend/: REST API with NestJS and tRPC
  • apps/frontend/: Next.js 15 application with React 19
  • Shared Packages (packages/)

  • packages/database/: Database layer with Drizzle ORM (PostgreSQL)
  • packages/schemas/: TypeScript schemas and types using Drizzle ORM
  • packages/trpc/: Shared tRPC client and server
  • packages/eslint-config/: ESLint configurations
  • packages/typescript-config/: TypeScript configurations
  • Technology Stack

  • Frontend: Next.js 15, React 19, Tailwind CSS v4, Shadcn UI
  • Backend: NestJS, nestjs-trpc, TypeScript
  • Database: PostgreSQL with Drizzle ORM
  • API: tRPC with Zod validation
  • Package Manager: Bun
  • Monorepo: Turborepo
  • What is tRPC and Why Do We Use It?

    What is tRPC?

    tRPC (TypeScript Remote Procedure Call) is a library that allows for creating fully type-safe APIs between the client and the server, eliminating the need to manually generate types or maintain separate API contracts.

    Principle: A Single Source of Truth

    In our project, tRPC acts as a single source of truth for all communication between the frontend and backend:

    text
    ┌─────────────────┐      ┌──────────────────┐      ┌─────────────────┐
    │   FRONTEND      │      │       tRPC       │      │    BACKEND      │
    │   (Next.js)     │◄────►│ (Single Source   │◄────►│   (NestJS)      │
    │                 │      │    of Truth)     │      │                 │
    │ • Auto-inferred │      │ • Procedures     │      │ • Definitions   │
    │   Types         │      │ • Zod Validation │      │ • Business Logic│
    │ • Autocomplete  │      │ • Type Inference │      │ • Database      │
    └─────────────────┘      └──────────────────┘      └─────────────────┘

    Benefits in Our Project

  • Complete Type Safety: Types are defined once in the backend and automatically propagate to the frontend. Type errors are caught at compile-time, not runtime.
  • No Code Duplication: No need to write types in both the frontend and backend or maintain separate API documentation. Changes in the backend are immediately reflected in the frontend.
  • Faster Development: Get intelligent autocompletion in the IDE, safe refactoring across the stack, and immediate detection of breaking changes.
  • Automatic Validation: Zod schemas validate input and output data, running on both client and server for clear, consistent errors.
  • Practical Example: Complete Workflow

    1. Definition in the Backend (apps/backend/src/processes/processes.router.ts):

    typescript
    import { Input, Mutation, Query, Router } from 'nestjs-trpc';
    import { ProcessesService } from './processes.service';
    import { z } from 'zod';
    import { processSchema, createProcessSchema, type CreateProcessInput } from '@repo/schemas';
    
    @Router({ alias: 'processes' })
    export class ProcessesRouter {
      constructor(private readonly processesService: ProcessesService) {}
    
      @Query({
        output: z.array(processSchema),
      })
      getAll() {
        return this.processesService.findAll();
      }
    
      @Mutation({
        input: createProcessSchema,
        output: processSchema,
      })
      create(@Input() input: CreateProcessInput) {
        return this.processesService.create(input);
      }
    }

    2. Automatic Usage in the Frontend (apps/frontend/src/app/(dashboard)/processes/_components/process-list.tsx):

    typescript
    'use client';
    import { trpc } from '@repo/trpc/client';
    
    export function ProcessList() {
      // ← Types are fully inferred, no duplication
      const { data: processes } = trpc.processes.getAll.useQuery();
      const createProcess = trpc.processes.create.useMutation();
    
      // ← TypeScript knows exactly what properties 'processes' has
      return (
        <div>
          {processes?.map((process) => (
            <div key={process.id}>{process.name}</div> // ← Autocomplete
          ))}
        </div>
      );
    }

    3. What You DON’T Need to Do:

  • Write duplicate interfaces in the frontend.
  • Maintain separate API documentation.
  • Manually generate types.
  • Manually validate data in the frontend.
  • Manually handle endpoint URLs.
  • Comparison: With and Without tRPC

    Without tRPC (Traditional):

    typescript
    // Backend - types.ts
    interface Process {
      id: string;
      name: string;
    }
    
    // Frontend - types.ts (DUPLICATED!)
    interface Process {
      id: string;
      name: string; // What if the backend changes this to 'title'?
    }
    
    // Frontend - api.ts
    const getProcesses = async (): Promise<Process[]> => {
      const response = await fetch('/api/processes'); // Manual URL
      return response.json(); // No validation
    };

    With tRPC (Our Approach):

    typescript
    // Backend only - single definition in the router
    @Query({ output: z.array(processSchema) })
    getAll() {
      return this.processesService.findAll();
    }
    
    // Frontend - direct usage with full type safety
    const { data } = trpc.processes.getAll.useQuery(); // Everything is automatic!

    Data Flow in Our Project

  • Zod Schemas (packages/schemas/) define the data structure.
  • NestJS Backend (apps/backend/) exposes tRPC procedures.
  • tRPC Package (packages/trpc/) contains the shared client and types.
  • Next.js Frontend (apps/frontend/) automatically consumes them with full type safety.
  • This approach ensures that any change in the backend is immediately reflected in the frontend, eliminating bugs from desynchronization and accelerating development.

    Environment Setup

    Prerequisites

  • Node.js >= 18
  • Bun >= 1.1.0
  • PostgreSQL
  • Installation

    bash
    # Install dependencies from the root of the monorepo
    bun install
    
    # Configure environment variables by copying the example file
    cp .env.example .env
    
    # Edit the .env file with your DATABASE_URL
    # Example:
    DATABASE_URL="postgresql://user:password@localhost:5432/your_db"
    
    # Build all shared packages and apps
    bun run build

    Backend Development Workflow

    Backend Structure

    text
    apps/backend/src/
    ├── main.ts                 # Application entry point
    ├── app.module.ts           # Main NestJS module
    ├── trpc-router.ts          # Standalone tRPC router definition
    └── [feature]/
        ├── [feature].module.ts
        ├── [feature].service.ts
        ├── [feature].controller.ts # (Optional) REST endpoints
        └── [feature].router.ts     # tRPC procedures for the feature

    Creating a New Module

  • Create the file structure:
  • Implement the service (users.service.ts):
  • Create the tRPC Router (users.router.ts):
  • Create the NestJS module (users.module.ts):
  • Register the new module in app.module.ts:
  • Backend Development Commands

    All commands should be run from the root of the monorepo.

    bash
    # Run backend in development mode
    bun run dev:backend
    
    # Build backend for production
    turbo build --filter=backend
    
    # Run backend tests
    turbo run test --filter=backend
    
    # Lint backend code
    turbo run lint --filter=backend

    Frontend Development Workflow

    Frontend Structure

    text
    apps/frontend/src/
    ├── app/
    │   ├── layout.tsx         # Root layout
    │   ├── page.tsx           # Main page
    │   └── (dashboard)/       # Route group for authenticated routes
    │       └── [feature]/
    │           ├── page.tsx
    │           ├── _components/ # Feature-specific components
    │           └── _lib/        # Hooks, types, and utilities
    ├── components/
    │   ├── ui/                # Shadcn UI components
    │   └── ...                # Other shared components
    └── lib/
        └── ...                # Shared utilities

    Installing Shadcn UI Components

    To add new UI components, run the following command from the monorepo root:

    bash
    # Example: Install a breadcrumb component
    bunx shadcn-ui@latest add breadcrumb --cwd apps/frontend

    Creating a New Page

  • Create the feature structure:
  • Implement component (_components/analytics-chart.tsx):
  • Create the page (page.tsx):
  • Frontend Development Commands

    bash
    # Run frontend in development mode
    bun run dev:frontend
    
    # Build frontend for production
    turbo build --filter=frontend
    
    # Lint frontend code
    turbo run lint --filter=frontend
    
    # Generate API types from backend schema
    # Note: Requires the backend dev server to be running
    bun run generate-api --filter=frontend

    Database Management

    Database Schema

    The main tables include: - users: User management with roles - processes: Business process entities - roles: Role-based access control - analysis_results: Analysis results

    Creating New Tables

  • Define the Drizzle schema in a new file, e.g., packages/database/src/tables/new_table.ts:
  • Export the new table schema from packages/database/src/index.ts:
  • Create corresponding Zod schemas in packages/schemas/src/api/new-table.schema.ts:
  • Database Commands

    bash
    # Generate a new migration based on schema changes
    bun run db:generate
    
    # Apply all pending migrations to the database
    bun run db:migrate
    
    # Open Drizzle Studio to view and manage data
    bun run db:studio

    tRPC Integration

    Client Configuration (Frontend)

    The tRPC client is configured in apps/frontend/src/app/layout.tsx via a provider:

    typescript
    import { TrpcProvider } from '@repo/trpc/client/providers/TrpcProvider';
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en">
          <body>
            <TrpcProvider>{children}</TrpcProvider>
          </body>
        </html>
      );
    }

    Usage in Components

    typescript
    'use client';
    import { trpc } from '@repo/trpc/client';
    
    export function ProcessList() {
      const utils = trpc.useUtils();
      const { data: processes, isLoading, error } = trpc.processes.getAll.useQuery();
      const createProcess = trpc.processes.create.useMutation({
        onSuccess: () => {
          // Invalidate the query to refetch data automatically
          utils.processes.getAll.invalidate();
        },
      });
    
      const handleCreate = async (data: { name: string }) => {
        try {
          await createProcess.mutateAsync(data);
        } catch (error) {
          console.error('Error creating process:', error);
        }
      };
    
      if (isLoading) return <div>Loading...</div>;
      if (error) return <div>Error: {error.message}</div>;
    
      return (
        <div>
          {processes?.map((process) => (
            <div key={process.id}>{process.name}</div>
          ))}
        </div>
      );
    }

    Available Endpoints

  • tRPC: http://localhost:4000/api/trpc
  • API Docs: http://localhost:4000/docs
  • Useful Commands

    General Development

    bash
    # Start all applications in development mode
    bun run dev
    
    # Build all applications for production
    bun run build
    
    # Run linters across the entire monorepo
    bun run lint
    
    # Format all code with Prettier
    bun run format
    
    # Run TypeScript type checking
    bun run check-types

    Package Management

    bash
    # Install a dependency in a specific workspace (e.g., backend)
    bun add <package-name> --filter=backend
    
    # Build only specific packages
    turbo build --filter=@repo/database --filter=@repo/schemas

    Best Practices

  • Code Language: All code, comments, and variables should be in English.
  • tRPC First: Prioritize tRPC for client-server communication over REST.
  • Schema Synchronization: Keep Drizzle and Zod schemas synchronized.
  • Build Dependencies: Always build packages/database and packages/schemas after changes before running the backend.
  • Run from Root: Execute all commands from the monorepo root for consistency.
  • CORS: Configure CORS in the backend with explicit allowed origins for security.
  • © 2025 - Tyler Miranda Hayashi