Project Architecture & Shared Types
Design a full-stack TypeScript architecture with shared types between frontend and backend — the key advantage of TypeScript across the entire stack.
55 min•By Priygop Team•Last updated: Feb 2026
Monorepo Architecture
- Project Structure: packages/shared (types and utilities), packages/server (Express API), packages/client (React frontend). Shared types imported by both server and client — single source of truth
- Shared Types: export interface User { id: number; email: string; name: string; role: 'admin' | 'user'; createdAt: string; }. Define once in packages/shared, import in both server response types and client state types
- API Contract: export interface ApiResponse<T> { success: boolean; data?: T; error?: string; pagination?: { page: number; limit: number; total: number; totalPages: number; } }. Both server and client agree on response shape at compile time
- Request DTOs: export interface CreateUserDTO { email: string; name: string; password: string; } export type UpdateUserDTO = Partial<Omit<CreateUserDTO, 'password'>>. Partial makes all fields optional for patches
- Route Definitions: export const API_ROUTES = { users: { list: '/api/users', create: '/api/users', getById: (id: number) => `/api/users/${id}` as const, update: (id: number) => `/api/users/${id}` as const } } as const — type-safe URL builder shared across stack
- Environment Types: declare namespace NodeJS { interface ProcessEnv { DATABASE_URL: string; JWT_SECRET: string; PORT: string; NODE_ENV: 'development' | 'production' | 'test'; } } — no more process.env.MISSING_VAR runtime crashes
TypeScript Configuration
- Base tsconfig: { 'compilerOptions': { 'strict': true, 'noUncheckedIndexedAccess': true, 'exactOptionalPropertyTypes': true } } — strict mode catches the most bugs. noUncheckedIndexedAccess makes array/object access return T | undefined
- Server tsconfig: extends base config, adds 'module': 'commonjs' for Node.js, 'outDir': './dist', 'rootDir': './src'. Target 'ES2022' for modern Node features (top-level await, Array.at())
- Client tsconfig: extends base config, 'jsx': 'react-jsx' for React, 'module': 'ESNext' for tree-shaking, 'paths': { '@shared/*': ['../shared/src/*'] } for clean imports
- Path Aliases: '@shared/types' maps to packages/shared/src/types — import { User } from '@shared/types' instead of import { User } from '../../shared/src/types'. Cleaner imports, easier refactoring
- Strict Null Checks: With strict mode, TypeScript tracks null/undefined everywhere — const user = users.find(u => u.id === id); user is User | undefined. You MUST check before accessing: if (!user) throw new NotFoundError()
- Declaration Files: packages/shared generates .d.ts files — other packages import types without importing runtime code. 'declaration': true, 'declarationMap': true in tsconfig for source-mapped type definitions