From Zero to AI

Lesson 6.2: Project Structure

Duration: 50 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  • Organize TypeScript projects using industry-standard patterns
  • Create logical folder structures for different project types
  • Separate concerns using models, services, and utilities
  • Configure TypeScript for multi-file projects
  • Understand when to use different organizational patterns

Why Project Structure Matters

A well-organized project is easier to:

  • Navigate: Find code quickly when you need it
  • Maintain: Make changes without breaking unrelated code
  • Scale: Add features without creating chaos
  • Test: Test components in isolation
  • Collaborate: Multiple developers can work without conflicts

Bad structure leads to "spaghetti code" where everything depends on everything else.


Basic Project Structure

Here is a minimal TypeScript project structure:

my-project/
├── src/
│   ├── index.ts        # Entry point
│   ├── types.ts        # Shared type definitions
│   └── utils.ts        # Utility functions
├── dist/               # Compiled JavaScript (generated)
├── package.json
└── tsconfig.json

The src folder contains source code, dist contains compiled output.


Standard Folder Structure

For larger projects, organize by purpose:

my-project/
├── src/
│   ├── index.ts           # Application entry point
│   ├── models/            # Data structures and types
│   │   ├── user.ts
│   │   ├── product.ts
│   │   └── index.ts       # Barrel export
│   ├── services/          # Business logic
│   │   ├── user-service.ts
│   │   ├── product-service.ts
│   │   └── index.ts
│   ├── utils/             # Helper functions
│   │   ├── validation.ts
│   │   ├── formatting.ts
│   │   └── index.ts
│   ├── config/            # Configuration
│   │   └── app-config.ts
│   └── types/             # Global type definitions
│       └── index.ts
├── tests/                 # Test files
│   ├── models/
│   ├── services/
│   └── utils/
├── dist/                  # Compiled output
├── package.json
└── tsconfig.json

Organizing by Feature

An alternative is organizing by feature instead of by type:

my-project/
├── src/
│   ├── index.ts
│   ├── users/             # Everything related to users
│   │   ├── user.model.ts
│   │   ├── user.service.ts
│   │   ├── user.utils.ts
│   │   └── index.ts
│   ├── products/          # Everything related to products
│   │   ├── product.model.ts
│   │   ├── product.service.ts
│   │   ├── product.utils.ts
│   │   └── index.ts
│   ├── orders/            # Everything related to orders
│   │   ├── order.model.ts
│   │   ├── order.service.ts
│   │   └── index.ts
│   └── shared/            # Shared across features
│       ├── utils/
│       ├── types/
│       └── index.ts
├── tests/
├── dist/
├── package.json
└── tsconfig.json

When to use feature-based structure:

  • Large applications with distinct features
  • Teams working on different features
  • Features that might become separate packages

Models Folder

Models define the shape of your data. They contain interfaces, types, and classes:

// src/models/user.ts
export interface User {
  id: number;
  username: string;
  email: string;
  createdAt: Date;
}

export interface CreateUserInput {
  username: string;
  email: string;
  password: string;
}

export interface UpdateUserInput {
  username?: string;
  email?: string;
}

export type UserRole = 'admin' | 'user' | 'guest';
// src/models/product.ts
export interface Product {
  id: number;
  name: string;
  description: string;
  price: number;
  category: ProductCategory;
  inStock: boolean;
}

export type ProductCategory = 'electronics' | 'clothing' | 'books' | 'other';

export interface ProductFilter {
  category?: ProductCategory;
  minPrice?: number;
  maxPrice?: number;
  inStockOnly?: boolean;
}

Services Folder

Services contain business logic. They use models and provide operations:

// src/services/user-service.ts
import type { CreateUserInput, UpdateUserInput, User } from '../models/user';

export class UserService {
  private users: User[] = [];
  private nextId = 1;

  create(input: CreateUserInput): User {
    const user: User = {
      id: this.nextId++,
      username: input.username,
      email: input.email,
      createdAt: new Date(),
    };
    this.users.push(user);
    return user;
  }

  findById(id: number): User | undefined {
    return this.users.find((user) => user.id === id);
  }

  findByEmail(email: string): User | undefined {
    return this.users.find((user) => user.email === email);
  }

  update(id: number, input: UpdateUserInput): User | undefined {
    const user = this.findById(id);
    if (!user) return undefined;

    if (input.username) user.username = input.username;
    if (input.email) user.email = input.email;

    return user;
  }

  delete(id: number): boolean {
    const index = this.users.findIndex((user) => user.id === id);
    if (index === -1) return false;

    this.users.splice(index, 1);
    return true;
  }

  findAll(): User[] {
    return [...this.users];
  }
}

Utils Folder

Utilities are helper functions that can be used anywhere:

// src/utils/validation.ts
export function isValidEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

export function isValidUsername(username: string): boolean {
  // 3-20 characters, alphanumeric and underscores only
  const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
  return usernameRegex.test(username);
}

export function isValidPassword(password: string): boolean {
  // At least 8 characters, one uppercase, one lowercase, one number
  return (
    password.length >= 8 &&
    /[A-Z]/.test(password) &&
    /[a-z]/.test(password) &&
    /[0-9]/.test(password)
  );
}
// src/utils/formatting.ts
export function formatDate(date: Date): string {
  return date.toISOString().split('T')[0];
}

export function formatCurrency(amount: number, currency = 'USD'): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
  }).format(amount);
}

export function capitalize(str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}

export function slugify(str: string): string {
  return str
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '');
}

Types Folder

Global types that are used across the application:

// src/types/index.ts

// Generic result type for operations
export interface Result<T> {
  success: boolean;
  data?: T;
  error?: string;
}

// Pagination
export interface PaginationParams {
  page: number;
  limit: number;
}

export interface PaginatedResult<T> {
  items: T[];
  total: number;
  page: number;
  totalPages: number;
}

// Common ID type
export type ID = number | string;

// Timestamp fields
export interface Timestamps {
  createdAt: Date;
  updatedAt: Date;
}

Config Folder

Configuration values for your application:

// src/config/app-config.ts
export const config = {
  app: {
    name: 'My Application',
    version: '1.0.0',
    environment: process.env.NODE_ENV || 'development',
  },
  pagination: {
    defaultLimit: 10,
    maxLimit: 100,
  },
  validation: {
    usernameMinLength: 3,
    usernameMaxLength: 20,
    passwordMinLength: 8,
  },
} as const;

// Type for the config
export type AppConfig = typeof config;

The Entry Point

The main index.ts ties everything together:

// src/index.ts
import { config } from './config/app-config';
import { ProductService } from './services/product-service';
import { UserService } from './services/user-service';

// Initialize services
const userService = new UserService();
const productService = new ProductService();

// Application startup
function main(): void {
  console.log(`Starting ${config.app.name} v${config.app.version}`);
  console.log(`Environment: ${config.app.environment}`);

  // Create sample data
  const user = userService.create({
    username: 'johndoe',
    email: 'john@example.com',
    password: 'Password123',
  });

  console.log('Created user:', user);

  // Your application logic here
}

main();

TypeScript Configuration

Configure tsconfig.json for your project structure:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}

Key options:

  • rootDir: Where your source files are
  • outDir: Where compiled files go
  • include: Which files to compile
  • exclude: Which files to ignore

Path Aliases

For cleaner imports, configure path aliases:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@models/*": ["models/*"],
      "@services/*": ["services/*"],
      "@utils/*": ["utils/*"],
      "@config/*": ["config/*"],
      "@types/*": ["types/*"]
    }
  }
}

Now you can import like this:

// Before: relative paths can get messy
import { User } from "../../../models/user";
import { formatDate } from "../../utils/formatting";

// After: clean path aliases
import { User } from "@models/user";
import { formatDate } from "@utils/formatting";

Note: You may need tsconfig-paths or bundler configuration to resolve these at runtime.


Naming Conventions

Consistent naming makes projects easier to navigate:

Files:

  • Use kebab-case: user-service.ts, date-utils.ts
  • Or camelCase: userService.ts, dateUtils.ts
  • Choose one and be consistent

Classes:

  • PascalCase: UserService, ProductModel

Interfaces and Types:

  • PascalCase: User, CreateUserInput
  • Some prefix with I: IUser (less common in TypeScript)

Functions and Variables:

  • camelCase: findById, formatDate

Constants:

  • UPPER_SNAKE_CASE: MAX_RETRIES, DEFAULT_TIMEOUT
  • Or camelCase for object constants: config

Common Patterns

Pattern 1: Separation of Concerns

Each module has one responsibility:

models/     - Data shapes only (no logic)
services/   - Business logic (uses models)
utils/      - Pure helper functions (no state)
config/     - Configuration values

Pattern 2: Dependency Direction

Dependencies should flow in one direction:

index.ts (entry)
    ↓
services/ (business logic)
    ↓
models/ (data structures)
    ↓
utils/ (helpers)

Services can use models and utils. Models should not import services.

Pattern 3: Index Files

Each folder has an index.ts that exports its public API:

// models/index.ts
export * from './user';
export * from './product';
export * from './order';

This enables clean imports:

import { Order, Product, User } from './models';

Exercises

Exercise 1: Create Project Structure

Create the following project structure with basic implementations:

task-manager/
├── src/
│   ├── index.ts
│   ├── models/
│   │   ├── task.ts       # Task interface
│   │   └── index.ts
│   ├── services/
│   │   ├── task-service.ts  # CRUD operations
│   │   └── index.ts
│   └── utils/
│       ├── date-utils.ts    # Date formatting
│       └── index.ts
├── package.json
└── tsconfig.json
Solution
// src/models/task.ts
export interface Task {
  id: number;
  title: string;
  description: string;
  completed: boolean;
  dueDate: Date | null;
  createdAt: Date;
}

export interface CreateTaskInput {
  title: string;
  description?: string;
  dueDate?: Date;
}

export type TaskStatus = "pending" | "completed" | "overdue";

// src/models/index.ts
export * from "./task";

// src/utils/date-utils.ts
export function formatDate(date: Date): string {
  return date.toLocaleDateString("en-US", {
    year: "numeric",
    month: "short",
    day: "numeric"
  });
}

export function isOverdue(date: Date): boolean {
  return date < new Date();
}

export function daysUntil(date: Date): number {
  const now = new Date();
  const diff = date.getTime() - now.getTime();
  return Math.ceil(diff / (1000 * 60 * 60 * 24));
}

// src/utils/index.ts
export * from "./date-utils";

// src/services/task-service.ts
import type { Task, CreateTaskInput } from "../models";
import { isOverdue } from "../utils";

export class TaskService {
  private tasks: Task[] = [];
  private nextId = 1;

  create(input: CreateTaskInput): Task {
    const task: Task = {
      id: this.nextId++,
      title: input.title,
      description: input.description || "",
      completed: false,
      dueDate: input.dueDate || null,
      createdAt: new Date()
    };
    this.tasks.push(task);
    return task;
  }

  findAll(): Task[] {
    return [...this.tasks];
  }

  findById(id: number): Task | undefined {
    return this.tasks.find(task => task.id === id);
  }

  complete(id: number): Task | undefined {
    const task = this.findById(id);
    if (task) {
      task.completed = true;
    }
    return task;
  }

  delete(id: number): boolean {
    const index = this.tasks.findIndex(task => task.id === id);
    if (index === -1) return false;
    this.tasks.splice(index, 1);
    return true;
  }

  getOverdue(): Task[] {
    return this.tasks.filter(
      task => !task.completed && task.dueDate && isOverdue(task.dueDate)
    );
  }
}

// src/services/index.ts
export * from "./task-service";

// src/index.ts
import { TaskService } from "./services";
import { formatDate } from "./utils";

const taskService = new TaskService();

// Create some tasks
taskService.create({
  title: "Learn TypeScript modules",
  description: "Complete lesson 6.2"
});

taskService.create({
  title: "Practice project structure",
  dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days from now
});

// Display tasks
const tasks = taskService.findAll();
tasks.forEach(task => {
  console.log(`[${task.completed ? "x" : " "}] ${task.title}`);
  if (task.dueDate) {
    console.log(`    Due: ${formatDate(task.dueDate)}`);
  }
});

Exercise 2: Add Validation

Add a validation.ts file to utils and use it in the service:

// src/utils/validation.ts
// Implement: validateTaskTitle, validateTaskDescription

// Use in task-service.ts to validate input
Solution
// Updated src/services/task-service.ts
import type { CreateTaskInput, Task } from '../models';
import { isOverdue, validateTaskDescription, validateTaskTitle } from '../utils';

// src/utils/validation.ts
export interface ValidationResult {
  valid: boolean;
  error?: string;
}

export function validateTaskTitle(title: string): ValidationResult {
  if (!title || title.trim().length === 0) {
    return { valid: false, error: 'Title is required' };
  }
  if (title.length > 100) {
    return { valid: false, error: 'Title must be 100 characters or less' };
  }
  return { valid: true };
}

export function validateTaskDescription(description: string): ValidationResult {
  if (description.length > 500) {
    return { valid: false, error: 'Description must be 500 characters or less' };
  }
  return { valid: true };
}

// src/utils/index.ts
export * from './date-utils';
export * from './validation';

export class TaskService {
  private tasks: Task[] = [];
  private nextId = 1;

  create(input: CreateTaskInput): Task {
    // Validate title
    const titleValidation = validateTaskTitle(input.title);
    if (!titleValidation.valid) {
      throw new Error(titleValidation.error);
    }

    // Validate description
    if (input.description) {
      const descValidation = validateTaskDescription(input.description);
      if (!descValidation.valid) {
        throw new Error(descValidation.error);
      }
    }

    const task: Task = {
      id: this.nextId++,
      title: input.title.trim(),
      description: input.description?.trim() || '',
      completed: false,
      dueDate: input.dueDate || null,
      createdAt: new Date(),
    };
    this.tasks.push(task);
    return task;
  }

  // ... rest of the methods
}

Key Takeaways

  1. Organize by purpose: Models, services, utils, config
  2. Or organize by feature: User, product, order folders
  3. Use index files: Create barrel exports for clean imports
  4. Keep dependencies flowing one way: Services use models, not vice versa
  5. Separate concerns: Each file has one responsibility
  6. Be consistent: Pick naming conventions and stick to them
  7. Configure TypeScript: Set up paths in tsconfig.json

Resources

Resource Type Description
TypeScript Project References Documentation Large project organization
Node.js Best Practices Guide Industry-standard patterns
Clean Code TypeScript Guide Clean code principles

Next Lesson

Now that you understand project structure, let us learn about barrel exports for cleaner module APIs.

Continue to Lesson 6.3: Barrel Exports