Organizing Code with Copilot
A well-organized code structure is fundamental for long-term maintainability. In this article, we'll see how to use Copilot to define folder structure, naming conventions, configuration management, and patterns for a clean, professional codebase.
Code Organization Principles
- Cohesion: Related files stay close together
- Predictability: Easy to find where everything is
- Scalability: Structure works even as project grows
- Consistency: Same conventions applied everywhere
- Discoverability: A new developer finds what they need quickly
Series Overview
| # | Module | Status |
|---|---|---|
| 1 | Foundation - Copilot as Partner | Completed |
| 2 | Ideation and Requirements | Completed |
| 3 | Backend and Frontend Architecture | Completed |
| 4 | Code Structure and Organization | You are here |
| 5 | Prompt Engineering and MCP Agents | Next |
| 6 | Testing and Quality | |
| 7 | Documentation | |
| 8 | Deploy and DevOps | |
| 9 | Evolution and Maintenance |
Complete Backend Folder Structure
Create a detailed folder structure for a Node.js + Express + TypeScript backend.
PROJECT: TaskFlow (time tracking + task management)
ARCHITECTURE: Layered (Controller - Service - Repository)
Requirements:
- Clean Architecture layers clearly separated
- Modular organization by feature/domain
- Clear separation of concerns
- Support for unit, integration, and e2e testing
- Environment-based configuration
- Database migrations and seeds
- Shared utilities and helpers
- API documentation structure
Show the complete tree with explanations for each folder/file.
Recommended Backend Structure
backend/
|-- src/
| |-- config/ # Application configuration
| | |-- index.ts # Main config export (loads from env)
| | |-- database.config.ts # Database connection settings
| | |-- auth.config.ts # JWT, OAuth settings
| | |-- cors.config.ts # CORS policy
| | |-- rate-limit.config.ts # Rate limiting rules
| |
| |-- modules/ # Feature modules (domain-driven)
| | |-- users/
| | | |-- users.controller.ts # HTTP handling
| | | |-- users.service.ts # Business logic
| | | |-- users.repository.ts # Data access
| | | |-- users.routes.ts # Route definitions
| | | |-- dto/ # Data Transfer Objects
| | | | |-- create-user.dto.ts
| | | | |-- update-user.dto.ts
| | | | |-- user-response.dto.ts
| | | |-- entities/ # Domain entities
| | | | |-- user.entity.ts
| | | |-- __tests__/ # Module-specific tests
| | | |-- users.service.spec.ts
| | | |-- users.controller.spec.ts
| | |
| | |-- auth/
| | | |-- auth.controller.ts
| | | |-- auth.service.ts
| | | |-- strategies/ # Passport strategies
| | | | |-- jwt.strategy.ts
| | | | |-- local.strategy.ts
| | | |-- dto/
| | | |-- auth.routes.ts
| | |
| | |-- tasks/
| | | |-- tasks.controller.ts
| | | |-- tasks.service.ts
| | | |-- tasks.repository.ts
| | | |-- tasks.routes.ts
| | | |-- dto/
| | | |-- entities/
| | | |-- __tests__/
| | |
| | |-- time-entries/
| | |-- ... (same structure)
| |
| |-- shared/ # Shared across modules
| | |-- middleware/
| | | |-- auth.middleware.ts
| | | |-- validation.middleware.ts
| | | |-- rate-limit.middleware.ts
| | | |-- request-logger.middleware.ts
| | |-- guards/
| | | |-- roles.guard.ts
| | | |-- ownership.guard.ts
| | |-- decorators/
| | | |-- current-user.decorator.ts
| | | |-- roles.decorator.ts
| | |-- errors/
| | | |-- app-error.ts
| | | |-- validation.error.ts
| | | |-- not-found.error.ts
| | | |-- unauthorized.error.ts
| | |-- utils/
| | | |-- hash.util.ts
| | | |-- jwt.util.ts
| | | |-- pagination.util.ts
| | | |-- date.util.ts
| | |-- types/
| | |-- express.d.ts # Express type extensions
| | |-- common.types.ts
| |
| |-- database/ # Database setup
| | |-- connection.ts # DB connection pool
| | |-- migrations/ # Schema migrations
| | | |-- 001_create_users.ts
| | | |-- 002_create_tasks.ts
| | | |-- 003_create_time_entries.ts
| | |-- seeds/ # Test/dev data
| | | |-- users.seed.ts
| | | |-- tasks.seed.ts
| | |-- queries/ # Complex SQL queries
| | |-- reports.queries.ts
| |
| |-- api/ # API versioning
| | |-- v1/
| | | |-- index.ts # v1 router aggregator
| | |-- index.ts # Main API router
| |
| |-- app.ts # Express app setup
| |-- server.ts # Entry point (starts server)
|
|-- tests/ # Test files (by type)
| |-- unit/ # Isolated unit tests
| |-- integration/ # API integration tests
| | |-- users.integration.spec.ts
| |-- e2e/ # End-to-end tests
| |-- fixtures/ # Test data
| | |-- users.fixture.ts
| |-- helpers/ # Test utilities
| |-- db.helper.ts # Test DB setup/teardown
| |-- auth.helper.ts # Get test tokens
|
|-- scripts/ # Utility scripts
| |-- migrate.ts # Run migrations
| |-- seed.ts # Seed database
| |-- generate-types.ts # Generate TS from DB
|
|-- docs/ # Documentation
| |-- api/ # OpenAPI specs
| | |-- openapi.yaml
| |-- adr/ # Architecture Decision Records
| |-- 001-database-choice.md
|
|-- .env.example # Environment template
|-- .env.test # Test environment
|-- tsconfig.json
|-- jest.config.js
|-- package.json
|-- README.md
Complete Frontend Folder Structure
frontend/
|-- src/
| |-- app/
| | |-- core/ # Singleton services (providedIn: 'root')
| | | |-- services/
| | | | |-- api.service.ts # HTTP client wrapper
| | | | |-- auth.service.ts # Auth state management
| | | | |-- storage.service.ts # LocalStorage/SessionStorage
| | | | |-- toast.service.ts # Notification service
| | | | |-- theme.service.ts # Dark/light mode
| | | |-- guards/
| | | | |-- auth.guard.ts # Protect routes
| | | | |-- guest.guard.ts # Redirect if logged in
| | | | |-- unsaved-changes.guard.ts # Prevent accidental navigation
| | | |-- interceptors/
| | | | |-- auth.interceptor.ts # Add JWT to requests
| | | | |-- error.interceptor.ts # Global error handling
| | | | |-- loading.interceptor.ts # Show/hide loading
| | | | |-- retry.interceptor.ts # Retry failed requests
| | | |-- models/
| | | |-- api-response.model.ts
| | | |-- user.model.ts
| | |
| | |-- shared/ # Reusable components (import in features)
| | | |-- components/
| | | | |-- ui/ # Design system primitives
| | | | | |-- button/
| | | | | | |-- button.component.ts
| | | | | | |-- button.component.html
| | | | | | |-- button.component.scss
| | | | | | |-- button.component.spec.ts
| | | | | |-- input/
| | | | | |-- select/
| | | | | |-- checkbox/
| | | | | |-- modal/
| | | | | |-- tooltip/
| | | | | |-- dropdown/
| | | | |-- layout/ # Layout components
| | | | | |-- card/
| | | | | |-- page-header/
| | | | | |-- empty-state/
| | | | |-- feedback/ # Feedback components
| | | | |-- loading-spinner/
| | | | |-- skeleton/
| | | | |-- error-message/
| | | | |-- toast/
| | | |-- directives/
| | | | |-- click-outside.directive.ts
| | | | |-- autofocus.directive.ts
| | | | |-- debounce-click.directive.ts
| | | | |-- tooltip.directive.ts
| | | |-- pipes/
| | | | |-- time-ago.pipe.ts
| | | | |-- duration.pipe.ts
| | | | |-- truncate.pipe.ts
| | | | |-- highlight.pipe.ts
| | | |-- index.ts # Barrel export
| | |
| | |-- features/ # Feature modules (lazy loaded)
| | | |-- auth/
| | | | |-- components/
| | | | | |-- login-form/
| | | | | |-- register-form/
| | | | | |-- forgot-password-form/
| | | | |-- pages/
| | | | | |-- login.page.ts
| | | | | |-- register.page.ts
| | | | | |-- forgot-password.page.ts
| | | | |-- services/
| | | | | |-- auth-api.service.ts
| | | | |-- models/
| | | | | |-- auth.model.ts
| | | | |-- auth.routes.ts
| | | |
| | | |-- tasks/
| | | | |-- components/
| | | | | |-- task-list/
| | | | | |-- task-item/
| | | | | |-- task-form/
| | | | | |-- task-timer/
| | | | | |-- task-filters/
| | | | |-- pages/
| | | | | |-- tasks-list.page.ts
| | | | | |-- task-detail.page.ts
| | | | |-- services/
| | | | | |-- tasks-api.service.ts
| | | | |-- state/ # Feature-specific state
| | | | | |-- tasks.store.ts # Signals-based store
| | | | |-- models/
| | | | | |-- task.model.ts
| | | | |-- tasks.routes.ts
| | | |
| | | |-- dashboard/
| | | | |-- components/
| | | | | |-- stats-card/
| | | | | |-- recent-tasks/
| | | | | |-- time-summary/
| | | | | |-- quick-actions/
| | | | |-- pages/
| | | | | |-- dashboard.page.ts
| | | | |-- dashboard.routes.ts
| | | |
| | | |-- settings/
| | | |-- components/
| | | |-- pages/
| | | |-- settings.routes.ts
| | |
| | |-- layouts/ # Layout components
| | | |-- main-layout/
| | | | |-- main-layout.component.ts
| | | | |-- components/
| | | | |-- header/
| | | | |-- sidebar/
| | | | |-- footer/
| | | |-- auth-layout/
| | | |-- auth-layout.component.ts
| | |
| | |-- app.component.ts
| | |-- app.routes.ts # Root routes
| | |-- app.config.ts # App providers
| |
| |-- assets/
| | |-- images/
| | |-- icons/
| | | |-- svg/ # SVG icons
| | |-- fonts/
| |
| |-- styles/
| | |-- _variables.scss # Design tokens
| | |-- _mixins.scss # SCSS mixins
| | |-- _typography.scss # Typography rules
| | |-- _utilities.scss # Utility classes
| | |-- global.scss # Global styles
| |
| |-- environments/
| |-- environment.ts
| |-- environment.prod.ts
|
|-- angular.json
|-- tsconfig.json
|-- package.json
Naming Conventions
Consistent naming helps Copilot generate coherent code and makes the codebase more readable for all team members (including yourself in 6 months).
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| File (general) | kebab-case | user-profile.component.ts |
| Class | PascalCase | UserProfileComponent |
| Variable/Function | camelCase | getUserProfile() |
| Constant | SCREAMING_SNAKE_CASE | MAX_RETRY_COUNT |
| Interface | PascalCase (no I prefix) | UserProfile |
| Type | PascalCase | TaskStatus |
| Enum | PascalCase | UserRole |
| Enum member | SCREAMING_SNAKE_CASE | UserRole.ADMIN |
| Boolean variable | is/has/can prefix | isLoading, hasError |
| Event handler | on/handle prefix | onSubmit, handleClick |
| Observable | $ suffix | users$, isLoading$ |
| Signal | no suffix (Angular 18+) | users, isLoading |
Recommended File Suffixes
Backend
// Controllers
user.controller.ts -> UserController
// Services
user.service.ts -> UserService
// Repositories
user.repository.ts -> UserRepository
// Entities
user.entity.ts -> User
// DTOs
create-user.dto.ts -> CreateUserDto
update-user.dto.ts -> UpdateUserDto
// Middleware
auth.middleware.ts -> authMiddleware
// Guards
roles.guard.ts -> RolesGuard
// Routes
user.routes.ts -> userRoutes
Frontend (Angular)
// Components
user-list.component.ts -> UserListComponent
// Pages (Smart Components)
users-list.page.ts -> UsersListPage
// Services
user.service.ts -> UserService
user-api.service.ts -> UserApiService
// Models
user.model.ts -> User (interface)
// Guards
auth.guard.ts -> authGuard (function)
// Interceptors
auth.interceptor.ts -> authInterceptor
// Pipes
time-ago.pipe.ts -> TimeAgoPipe
// Directives
tooltip.directive.ts -> TooltipDirective
Separation of Concerns
Each file/class should have a single, clear responsibility. This makes code more testable, reusable, and maintainable.
Wrong: God Controller
// user.controller.ts - EVERYTHING together
class UserController {{ '{' }}
async create(req, res) {{ '{' }}
// 1. Validation (should be in middleware)
if (!req.body.email) {{ '{' }}
return res.status(400).json(...);
{{ '}' }}
if (!/^[^\s@]+@[^\s@]+$/.test(req.body.email)) {{ '{' }}
return res.status(400).json(...);
{{ '}' }}
// 2. Business logic (should be in service)
const exists = await db.query('SELECT...');
if (exists) throw new Error('...');
const hashedPassword = await bcrypt.hash(...);
// 3. Database (should be in repository)
await db.query('INSERT INTO users...');
// 4. Send email (should be in service)
await sendgrid.send({{ '{' }}
to: req.body.email,
subject: 'Welcome!',
...
{{ '}' }});
res.json({{ '{' }} success: true {{ '}' }});
{{ '}' }}
{{ '}' }}
Correct: Separate Responsibilities
// Controller: ONLY HTTP handling
async create(req, res, next) {{ '{' }}
try {{ '{' }}
const user = await this.userService
.createUser(req.body);
res.status(201).json({{ '{' }} data: user {{ '}' }});
{{ '}' }} catch (err) {{ '{' }}
next(err);
{{ '}' }}
{{ '}' }}
// Service: ONLY business logic
async createUser(dto: CreateUserDto) {{ '{' }}
await this.validateUniqueEmail(dto.email);
const user = new User(dto);
const saved = await this.userRepo.save(user);
await this.emailService.sendWelcome(saved);
return saved;
{{ '}' }}
// Repository: ONLY data access
async save(user: User): Promise<User> {{ '{' }}
return this.db.users.create(user);
{{ '}' }}
// EmailService: ONLY email
async sendWelcome(user: User) {{ '{' }}
await this.mailer.send({{ '{' }}...{{ '}' }});
{{ '}' }}
Configuration Management
Centralize configurations by environment with validation and type safety.
// config/index.ts
import dotenv from 'dotenv';
import {{ '{' }} z {{ '}' }} from 'zod';
// Load .env file
dotenv.config();
// Define schema with validation
const configSchema = z.object({{ '{' }}
// Environment
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
PORT: z.string().transform(Number).default('3000'),
// Database
DATABASE_URL: z.string().url(),
DATABASE_POOL_MIN: z.string().transform(Number).default('2'),
DATABASE_POOL_MAX: z.string().transform(Number).default('10'),
// Authentication
JWT_SECRET: z.string().min(32),
JWT_EXPIRES_IN: z.string().default('7d'),
BCRYPT_ROUNDS: z.string().transform(Number).default('12'),
// CORS
CORS_ORIGIN: z.string().default('http://localhost:4200'),
// External Services (optional in dev)
SENDGRID_API_KEY: z.string().optional(),
SENTRY_DSN: z.string().optional(),
{{ '}' }});
// Parse and validate
const parsed = configSchema.safeParse(process.env);
if (!parsed.success) {{ '{' }}
console.error('Invalid environment variables:');
console.error(parsed.error.format());
process.exit(1);
{{ '}' }}
// Export typed config
export const config = {{ '{' }}
env: parsed.data.NODE_ENV,
port: parsed.data.PORT,
isProduction: parsed.data.NODE_ENV === 'production',
isDevelopment: parsed.data.NODE_ENV === 'development',
database: {{ '{' }}
url: parsed.data.DATABASE_URL,
pool: {{ '{' }}
min: parsed.data.DATABASE_POOL_MIN,
max: parsed.data.DATABASE_POOL_MAX,
{{ '}' }},
{{ '}' }},
jwt: {{ '{' }}
secret: parsed.data.JWT_SECRET,
expiresIn: parsed.data.JWT_EXPIRES_IN,
{{ '}' }},
bcrypt: {{ '{' }}
rounds: parsed.data.BCRYPT_ROUNDS,
{{ '}' }},
cors: {{ '{' }}
origin: parsed.data.CORS_ORIGIN.split(','),
{{ '}' }},
sendgrid: {{ '{' }}
apiKey: parsed.data.SENDGRID_API_KEY,
{{ '}' }},
sentry: {{ '{' }}
dsn: parsed.data.SENTRY_DSN,
{{ '}' }},
{{ '}' }} as const;
// Type for config
export type Config = typeof config;
.env.example File
# =============================================================
# TaskFlow Backend Configuration
# Copy this file to .env and fill in your values
# =============================================================
# -------------------------------------------------------------
# Environment
# -------------------------------------------------------------
NODE_ENV=development
PORT=3000
# -------------------------------------------------------------
# Database (PostgreSQL)
# -------------------------------------------------------------
DATABASE_URL=postgresql://user:password@localhost:5432/taskflow
DATABASE_POOL_MIN=2
DATABASE_POOL_MAX=10
# -------------------------------------------------------------
# Authentication
# -------------------------------------------------------------
# Generate with: openssl rand -base64 32
JWT_SECRET=your-super-secret-key-at-least-32-chars
JWT_EXPIRES_IN=7d
BCRYPT_ROUNDS=12
# -------------------------------------------------------------
# CORS
# -------------------------------------------------------------
CORS_ORIGIN=http://localhost:4200
# -------------------------------------------------------------
# External Services (optional for development)
# -------------------------------------------------------------
# SENDGRID_API_KEY=SG.xxxxx
# SENTRY_DSN=https://xxxxx@sentry.io/xxxxx
Index Files and Barrel Exports
Use index.ts files for clean exports and simplified imports.
// =============================================================
// shared/components/index.ts - Barrel Export
// =============================================================
// UI Components
export {{ '{' }} ButtonComponent {{ '}' }} from './ui/button/button.component';
export {{ '{' }} InputComponent {{ '}' }} from './ui/input/input.component';
export {{ '{' }} SelectComponent {{ '}' }} from './ui/select/select.component';
export {{ '{' }} ModalComponent {{ '}' }} from './ui/modal/modal.component';
// Layout Components
export {{ '{' }} CardComponent {{ '}' }} from './layout/card/card.component';
export {{ '{' }} PageHeaderComponent {{ '}' }} from './layout/page-header/page-header.component';
export {{ '{' }} EmptyStateComponent {{ '}' }} from './layout/empty-state/empty-state.component';
// Feedback Components
export {{ '{' }} LoadingSpinnerComponent {{ '}' }} from './feedback/loading-spinner/loading-spinner.component';
export {{ '{' }} SkeletonComponent {{ '}' }} from './feedback/skeleton/skeleton.component';
export {{ '{' }} ToastComponent {{ '}' }} from './feedback/toast/toast.component';
// =============================================================
// Usage in feature module
// =============================================================
// BEFORE (without barrel)
import {{ '{' }} ButtonComponent {{ '}' }} from '../../../shared/components/ui/button/button.component';
import {{ '{' }} InputComponent {{ '}' }} from '../../../shared/components/ui/input/input.component';
import {{ '{' }} CardComponent {{ '}' }} from '../../../shared/components/layout/card/card.component';
// AFTER (with barrel)
import {{ '{' }}
ButtonComponent,
InputComponent,
CardComponent
{{ '}' }} from '@shared/components';
// =============================================================
// tsconfig.json - Path aliases
// =============================================================
{{ '{' }}
"compilerOptions": {{ '{' }}
"baseUrl": "./src",
"paths": {{ '{' }}
"@shared/*": ["app/shared/*"],
"@core/*": ["app/core/*"],
"@features/*": ["app/features/*"],
"@environments/*": ["environments/*"]
{{ '}' }}
{{ '}' }}
{{ '}' }}
Import Organization
// =============================================================
// Recommended import order
// =============================================================
// 1. Angular/Framework imports
import {{ '{' }} Component, OnInit, inject {{ '}' }} from '@angular/core';
import {{ '{' }} CommonModule {{ '}' }} from '@angular/common';
import {{ '{' }} RouterModule {{ '}' }} from '@angular/router';
import {{ '{' }} FormBuilder, ReactiveFormsModule {{ '}' }} from '@angular/forms';
// 2. Third-party library imports
import {{ '{' }} Observable {{ '}' }} from 'rxjs';
import {{ '{' }} map, catchError {{ '}' }} from 'rxjs/operators';
// 3. Core/Shared imports (using path aliases)
import {{ '{' }} AuthService {{ '}' }} from '@core/services/auth.service';
import {{ '{' }} ButtonComponent, CardComponent {{ '}' }} from '@shared/components';
import {{ '{' }} TimeAgoPipe {{ '}' }} from '@shared/pipes';
// 4. Feature-specific imports (relative paths)
import {{ '{' }} TasksApiService {{ '}' }} from './services/tasks-api.service';
import {{ '{' }} TaskItemComponent {{ '}' }} from './components/task-item/task-item.component';
import {{ '{' }} Task {{ '}' }} from './models/task.model';
// 5. Constants and types
import {{ '{' }} TASK_STATUS {{ '}' }} from './constants/task.constants';
import type {{ '{' }} TaskFilters {{ '}' }} from './types/task.types';
File Templates for Copilot
Create templates that Copilot can use as reference to generate consistent code.
// =============================================================
// Template: Angular Standalone Component
// =============================================================
import {{ '{' }} Component, Input, Output, EventEmitter, ChangeDetectionStrategy {{ '}' }} from '@angular/core';
import {{ '{' }} CommonModule {{ '}' }} from '@angular/common';
/**
* [ComponentName] - [Brief description]
*
* @example
* <app-[component-name]
* [inputProp]="value"
* (outputEvent)="handler($event)"
* />
*/
@Component({{ '{' }}
selector: 'app-[component-name]',
standalone: true,
imports: [CommonModule],
templateUrl: './[component-name].component.html',
styleUrl: './[component-name].component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
{{ '}' }})
export class [ComponentName]Component {{ '{' }}
// -------------------------------------------------------------
// Inputs
// -------------------------------------------------------------
@Input() inputProp: string = '';
// -------------------------------------------------------------
// Outputs
// -------------------------------------------------------------
@Output() outputEvent = new EventEmitter<void>();
// -------------------------------------------------------------
// Public methods
// -------------------------------------------------------------
onAction(): void {{ '{' }}
this.outputEvent.emit();
{{ '}' }}
{{ '}' }}
Code Organization Checklist
Checklist
| Item | Status |
|---|---|
| Folder structure defined and documented | |
| Naming conventions documented | |
| Path aliases configured in tsconfig | |
| Barrel exports for shared modules | |
| Config management centralized with validation | |
| .env.example with all variables | |
| ESLint/Prettier configured | |
| Husky + lint-staged for pre-commit hooks |
Conclusion
A well-organized code structure pays dividends throughout the project's lifetime. Investing time in defining clear conventions makes development faster, code more maintainable, and onboarding new contributors (or yourself in 6 months) much easier.
Key Takeaways
- Feature-Based Structure: Organize by feature, not by file type
- Naming Conventions: kebab-case for files, PascalCase for classes
- Separation of Concerns: Controller -> Service -> Repository
- Config Management: Centralize with schema validation (Zod)
- Barrel Exports: Index.ts for clean imports
- Path Aliases: @shared, @core, @features to avoid ../../../
Next Article
In the next article "Prompt Engineering and MCP Agents" we'll see advanced techniques for writing effective prompts and how to configure MCP agents for persistent context in your project.







