Turborepo Example with NestJS and Next.js
Table of Contents
Monorepo Architecture
This project uses Turborepo to manage a monorepo with the following applications and packages:
Applications (apps/)
Shared Packages (packages/)
Technology Stack
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:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ 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
Practical Example: Complete Workflow
1. Definition in the Backend (apps/backend/src/processes/processes.router.ts):
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):
'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:
Comparison: With and Without tRPC
Without tRPC (Traditional):
// 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):
// 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
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
Installation
# 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
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
Backend Development Commands
All commands should be run from the root of the monorepo.
# 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
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:
# Example: Install a breadcrumb component
bunx shadcn-ui@latest add breadcrumb --cwd apps/frontend
Creating a New Page
Frontend Development Commands
# 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
Database Commands
# 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:
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
'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
Useful Commands
General Development
# 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
# 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