Day One: Setting up Turbo and All Dependencies
In this post, we will set up a Turborepo for the "Utter Todo" project. We'll install t3-app
, Nest
, ESLint
, Prettier
, and TypeScript
, ensuring everything runs smoothly and is orchestrated by Turbo.
Initial Setup
First, let's create our project directory and initialize a Turbo repo:
mkdir utter-todo cd utter-todo pnpm dlx create-turbo@latest ./
Choose pnpm workspaces
as your package manager.
Next, let's clean up the initial Turbo installation by removing unnecessary packages. We'll start from scratch.
Configuring pnpm-workspace.yaml
Update pnpm-workspace.yaml
as follows:
# ./pnpm-workspace.yaml packages: - "apps/*" - "packages/*" - "config/*"
Updating the Root package.json
Modify the root package.json
:
// ./package.json { "name": "utter-todo", "private": true, "scripts": { "build": "turbo build", "dev": "turbo dev", "lint": "turbo lint", "format": "prettier --write \"**/*.{ts,tsx,md}\"" }, "devDependencies": { "prettier": "^3.2.5", "turbo": "^2.0.4", "typescript": "^5.4.5" }, "packageManager": "pnpm@9.1.1", "engines": { "node": ">=18" }, "workspaces": [ "config/*", "packages/*", "apps/*" ] }
Setting Up Configuration Packages
Let's set up the configuration packages for TypeScript, ESLint, and Prettier.
TypeScript Configuration
Create the TypeScript config package:
mkdir -p config/typescript-config cd config/typescript-config pnpm init
Add the following package.json
:
// ./config/typescript-config/package.json { "name": "@utter-todo/typescript-config", "version": "0.0.1", "private": true, "license": "MIT", "files": ["next.json", "node.json", "nest.json"] }
Refer to the rules for each environment here:
Prettier Configuration
Set up the Prettier config:
cd ../.. mkdir -p config/prettier cd config/prettier pnpm init pnpm add -D prettier prettier-plugin-tailwindcss touch index.mjs
Add the Prettier config:
// ./config/prettier/index.mjs /** @typedef {import('prettier').Config} PrettierConfig */ /** @type {PrettierConfig} */ const config = { plugins: ['prettier-plugin-tailwindcss'], printWidth: 80, tabWidth: 2, useTabs: false, semi: true, singleQuote: true, quoteProps: 'as-needed', jsxSingleQuote: false, trailingComma: 'es5', bracketSpacing: true, arrowParens: 'always', endOfLine: 'auto', bracketSameLine: false, } export default config
Update the package.json
:
// ./config/prettier/package.json { "name": "@utter-todo/prettier", "version": "0.0.1", "main": "index.mjs", "private": true, "license": "MIT", "devDependencies": { "prettier": "^3.2.5", "prettier-plugin-tailwindcss": "^0.6.4" } }
ESLint Configuration
Set up the ESLint config:
cd ../.. mkdir -p config/eslint-config cd config/eslint-config pnpm init pnpm add -D @rocketseat/eslint-config eslint-plugin-simple-import-sort
Update the package.json
:
// ./config/eslint-config/package.json { "name": "@utter-todo/eslint-config", "version": "0.0.1", "private": true, "files": [ "react.js", "next.js", "node.js" ], "devDependencies": { "@utter-todo/prettier": "workspace:*", "@rocketseat/eslint-config": "^2.2.2", "eslint-plugin-simple-import-sort": "^12.1.0" }, "eslintConfig": { "extends": ["./react.js"] } }
Refer to the rules for each environment here:
Checking Initial Setup
Back in the root directory, install dependencies and check if everything is running:
cd ../.. pnpm i pnpm run dev
Since we don't have any dev scripts in our packages yet, nothing will run, but there should be no errors.
Creating the Domain Package
Create the domain package for business logic:
mkdir -p packages/domain cd packages/domain pnpm init pnpm add -D typescript @types/node vite-tsconfig-paths vitest
Update the package.json
:
// ./packages/domain/package.json { "name": "@utter-todo/domain", "version": "0.0.1", "main": "src/index.ts", "scripts": { "test": "vitest run --dir src/use-cases", "test:watch": "vitest --dir src/use-cases" }, "license": "MIT", "devDependencies": { "@types/node": "^20.14.2", "@utter-todo/eslint-config": "workspace:*", "@utter-todo/prettier": "workspace:*", "@utter-todo/typescript-config": "workspace:*", "typescript": "^5.4.5", "vite-tsconfig-paths": "^4.3.2", "vitest": "^1.6.0" }, "prettier": "@utter-todo/prettier", "eslintConfig": { "extends": [ "@utter-todo/eslint-config/node" ] } }
Make some changes in the tsconfig.json:
// ./packages/domain/tsconfig.json { "extends": "@utter-todo/typescript-config/node.json", "include": [ "src/**/*", ], "compilerOptions": { "baseUrl": ".", "paths": { "@/*": [ "./src/*" ] }, "types": [ "vitest/globals" ], "allowJs": true, "incremental": true, "esModuleInterop": true, }, "exclude": [ "node_modules" ] }
Install local dependencies:
pnpm i
Create a draft for the Task entity to test if everything works:
mkdir -p src/entities cd src/entities touch task.ts
// ./packages/domain/entities/task.ts export class Task { private id: string; private title: string; constructor(id: string, title: string) { this.id = id; this.title = title; } }
Check if VS Code shows any errors or auto-fixes on save.
Setting Up the Web Package
Install the Web package:
cd ../.. mkdir -p apps/web cd apps/web pnpm create t3-app@latest ./
Follow the prompts:
- Yes for TypeScript
- Yes for Tailwind CSS
- No for tRPC
- None for authentication
- None for ORM
- Yes for Next.js App Router
- PostgreSQL for database provider
- No for initializing a Git repository
- Yes for running
pnpm install
- @/ for import alias
Update package.json
:
// ./apps/web/package.json { "name": "@utter-todo/web", ... "devDependencies": { "@utter-todo/eslint-config": "workspace:*", "@utter-todo/prettier": "workspace:*", "@utter-todo/typescript-config": "workspace:*", "@types/node": "^20.11.20", "@types/react": "^18.2.57", "@types/react-dom": "^18.2.19", "postcss": "^8.4.34", "tailwindcss": "^3.4.3", "typescript": "^5.4.2" }, ... " prettier": "@utter-todo/prettier", "eslintConfig": { "extends": [ "@utter-todo/eslint-config/next" ] } }
Check if everything is running:
cd ../.. pnpm i pnpm run dev
Install shadcn-ui
and vitest
:
pnpm add -D vitest vite-tsconfig-paths pnpm dlx shadcn-ui@latest init
Follow the prompts:
- Default style
- Slate base color
- Yes for CSS variables
Update tsconfig.json
:
// ./apps/web/tsconfig.json { "extends": "@utter-todo/typescript-config/next.json", "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] }
Setting Up the API Package
Install Nest.js for the API:
cd apps pnpm init pnpm add drizzle-orm pg zod @fastify/cors @fastify/jwt @fastify/swagger @fastify/swagger-ui dotenv-cli fastify fastify-plugin fastify-type-provider-zod jsonwebtoken zod pnpm add -D drizzle-kit @types/pg vitest @types/node
Update package.json
:
// ./apps/api/package.json { "name": "@utter-todo/api", "version": "0.0.1", "private": true, "license": "MIT", "scripts": { "dev": "tsx watch src/server.ts", "start": "node dist/server.js", "test:e2e": "vitest run --dir ./src/http/controllers --no-file-parallelism", "test:watch": "vitest --dir ./src/http/controllers --no-file-parallelism", "build": "tsup", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"" }, "dependencies": { "@fastify/cors": "^9.0.1", "@fastify/jwt": "^8.0.1", "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^4.0.0", "@utter-todo/domain": "workspace:*", "dotenv-cli": "^7.4.2", "drizzle-orm": "^0.31.2", "fastify": "^4.28.0", "fastify-plugin": "^4.5.1", "fastify-type-provider-zod": "^1.2.0", "jsonwebtoken": "^9.0.2", "pg": "^8.12.0", "zod": "^3.23.8" }, "devDependencies": { "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.3.1", "@types/pg": "^8.11.6", "@types/supertest": "^6.0.0", "@utter-todo/eslint-config": "workspace:*", "@utter-todo/prettier": "workspace:*", "@utter-todo/typescript-config": "workspace:*", "dotenv": "^16.4.5", "drizzle-kit": "^0.22.7", "supertest": "^6.3.3", "ts-loader": "^9.4.3", "ts-node": "^10.9.1", "tsup": "^8.1.0", "typescript": "^5.1.3", "vite-tsconfig-paths": "^4.3.2", "vitest": "^1.6.0" }, "prettier": "@utter-todo/prettier", "eslintConfig": { "extends": [ "@utter-todo/eslint-config/node" ] } }
Update tsconfig.json
:
// ./apps/api/tsconfig.json { "extends": "@utter-todo/typescript-config/node.json", "include": ["."], "exclude": ["node_modules", "dist"], "compilerOptions": { "outDir": "./dist", "baseUrl": ".", "types": ["vitest/globals"] } }
Set env variables for the API:
// ./apps/api/src/env.ts import { config } from 'dotenv' import { z } from 'zod' config() const envSchema = z.object({ API_PORT: z.coerce.number().default(4000), API_URL: z.string().url(), DB_URL: z.string().url(), DB_TEST_URL: z.string().url(), JWT_SECRET: z.string(), NODE_ENV: z.string().default('development'), }) const _env = envSchema.safeParse(process.env) if (_env.success === false) { console.error('❌ Invalid environment variables', _env.error.format()) throw new Error('Invalid environment variables') } export const env = _env.data
Set a basic fastify+zod+swagger app settings:
// ./apps/api/src/app.ts import fastifyCors from '@fastify/cors' import fastifyJwt from '@fastify/jwt' import fastifySwagger from '@fastify/swagger' import fastifySwaggerUI from '@fastify/swagger-ui' import fastify from 'fastify' import { jsonSchemaTransform, serializerCompiler, validatorCompiler, ZodTypeProvider, } from 'fastify-type-provider-zod' import { env } from './env' import { errorHandler } from './error-handler' import { createTaskController } from './http/controllers/create-task' import { deleteTaskController } from './http/controllers/delete-task' import { fetchTasksController } from './http/controllers/fetch-tasks' import { toggleTaskCompletedController } from './http/controllers/toggle-task-completed' export const app = fastify() app.withTypeProvider<ZodTypeProvider>() app.setSerializerCompiler(serializerCompiler) app.setValidatorCompiler(validatorCompiler) app.setErrorHandler(errorHandler) app.register(fastifySwagger, { openapi: { info: { title: 'API RESTFul - Utter Todo', description: '...', version: '1.0.0', }, components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', }, }, }, }, transform: jsonSchemaTransform, }) app.register(fastifySwaggerUI, { routePrefix: '/docs', }) app.register(fastifyJwt, { secret: env.JWT_SECRET, }) app.register(fastifyCors)
Finally, set the fastify app to listen on the API port:
// ./apps/api/src/server.ts import { app } from './app' import { env } from './env' app .listen({ host: '0.0.0.0', port: env.API_PORT, }) .then(() => { console.log('') console.log('🤘 Utter Todo API running!') })
Check if everything is running:
cd .. pnpm i pnpm run dev
Conclusion
We've successfully set up Turbo, configured our monorepo, and installed the main dependencies for our project. Next, we'll build the domain package with TDD!
Contribute to the Project
If you found this post helpful or have suggestions for improvement, feel free to check out the project repository on GitHub. You are welcome to fork the repository and submit a pull request. If you have any questions or want to discuss a topic, please open an issue. We appreciate your contributions!