We use cookies to enhance your experience on the site
CodeWorlds

Structuring Code in a Larger Project

After going through all the most important JavaScript techniques, it's time to look at the bigger picture — how to organize code in larger projects. Poor code structure can lead to a disaster similar to what we experienced in Jurassic Park — chaos, errors, and difficulty managing the system.

Why Project Structure Matters

Imagine all of Jurassic Park's infrastructure managed by one massive system. All enclosure security, dinosaur health monitoring, visitor flow management, catering, genetics labs — everything in one monolithic program. Disaster is just around the corner, right?

A well-designed project structure ensures:

  1. Maintainability — easier to find and fix bugs
  2. Scalability — ability to add new features without breaking existing ones
  3. Testability — ability to test components in isolation
  4. Collaboration ease — multiple developers can work in parallel on different parts
  5. Readability — easier to understand how the system works

Folder and File Organization

There are many approaches to organizing code in larger JavaScript projects. Let's look at a few proven patterns that would work in a Jurassic Park management system.

1. Type-Based Structure

One popular structure, especially in smaller apps, groups files by their type:

1jurassic-park-management/
2├── src/
3│   ├── components/          # UI components
4│   ├── services/            # API communication services
5│   ├── utils/               # Utility tools
6│   ├── models/              # Type and interface definitions
7│   ├── hooks/               # Custom hooks
8│   ├── constants/           # Application constants
9│   ├── assets/              # Images, fonts, etc.
10│   ├── styles/              # CSS/SCSS styles
11│   └── tests/               # Tests
12└── package.json

2. Feature-Based Structure

As the project grows, a feature-based structure is often better:

1jurassic-park-management/
2├── src/
3│   ├── features/
4│   │   ├── dinosaurs/
5│   │   │   ├── DinosaurList.jsx
6│   │   │   ├── DinosaurDetail.jsx
7│   │   │   ├── dinosaurService.js
8│   │   │   ├── dinosaurReducer.js
9│   │   │   └── dinosaur.test.js
10│   │   ├── security/
11│   │   │   ├── SecurityDashboard.jsx
12│   │   │   ├── securityService.js
13│   │   │   └── security.test.js
14│   │   └── monitoring/
15│   │       ├── MonitoringPanel.jsx
16│   │       ├── monitoringService.js
17│   │       └── monitoring.test.js
18│   ├── shared/
19│   │   ├── components/      # Shared components
20│   │   ├── utils/           # Shared utilities
21│   │   └── hooks/           # Shared hooks
22│   └── app/
23│       ├── store.js
24│       └── router.js
25└── package.json

3. Modular Structure (Domain-Driven)

For enterprise-level projects:

1jurassic-park-management/
2├── modules/
3│   ├── DinosaurModule/
4│   │   ├── domain/          # Business logic
5│   │   ├── infrastructure/  # External communication
6│   │   ├── presentation/    # UI
7│   │   └── index.js         # Public API
8│   ├── SecurityModule/
9│   └── MonitoringModule/
10├── shared/
11└── app/

Application Architecture

MVC (Model-View-Controller)

1// Model
2class DinosaurModel {
3  constructor(data) {
4    this.id = data.id;
5    this.name = data.name;
6    this.species = data.species;
7    this.health = data.health;
8  }
9
10  isHealthy() { return this.health > 60; }
11  isDangerous() { return this.dangerLevel > 7; }
12}
13
14// View
15class DinosaurView {
16  render(dinosaur) {
17    return `<div class="dino-card ${dinosaur.isHealthy() ? 'healthy' : 'sick'}">
18      <h3>${dinosaur.name}</h3>
19      <p>${dinosaur.species}</p>
20      <p>Health: ${dinosaur.health}%</p>
21    </div>`;
22  }
23}
24
25// Controller
26class DinosaurController {
27  constructor(model, view, service) {
28    this.model = model;
29    this.view = view;
30    this.service = service;
31  }
32
33  async loadAndDisplay(id) {
34    const data = await this.service.getDinosaur(id);
35    const dinosaur = new this.model(data);
36    return this.view.render(dinosaur);
37  }
38}

Code Modularization

Regardless of folder structure and architecture, the key is to properly split code into smaller, cohesive modules.

1. Modularization by Responsibility

Each module should have a clear, single responsibility:

1// dinosaurHealthMonitor.js - responsible only for monitoring dinosaur health
2export function checkVitalSigns(dinosaurId) {
3  //
4}
5
6export function alertHealthIssue(dinosaurId, issue) {
7  //
8}
9
10// enclosureSecurity.js - responsible only for enclosure security
11export function checkEnclosureSecurity(enclosureId) {
12  //
13}
14
15export function activateEmergencyProtocol(enclosureId) {
16  //
17}

2. Clean Interfaces Between Modules

Modules should communicate through clean, well-defined interfaces:

1// Higher-level module uses lower-level module interfaces
2import { checkVitalSigns } from './dinosaurHealthMonitor';
3import { checkEnclosureSecurity } from './enclosureSecurity';
4
5// parkSafetySystem.js
6export async function performSafetyCheck() {
7  const dinosaurs = await fetchAllDinosaurs();
8  const enclosures = await fetchAllEnclosures();
9
10  // Use other module interfaces
11  const healthIssues = dinosaurs.map(checkVitalSigns);
12  const securityIssues = enclosures.map(checkEnclosureSecurity);
13
14  return {
15    healthIssues: healthIssues.filter(issue => issue !== null),
16    securityIssues: securityIssues.filter(issue => issue !== null)
17  };
18}

3. Dependency Injection

Dependency injection is a technique that allows reversing dependencies between modules, making testing and changes easier:

1// Without DI
2function DinosaurMonitor() {
3  // Direct dependency
4  const vitalSignsMonitor = new VitalSignsMonitor();
5
6  return {
7    checkDinosaur(dinosaur) {
8      return vitalSignsMonitor.check(dinosaur);
9    }
10  };
11}
12
13// With DI
14function DinosaurMonitor(vitalSignsMonitor) {
15  return {
16    checkDinosaur(dinosaur) {
17      return vitalSignsMonitor.check(dinosaur);
18    }
19  };
20}
21
22// Now we can inject different implementations
23const realMonitor = DinosaurMonitor(new VitalSignsMonitor());
24const testMonitor = DinosaurMonitor(new MockVitalSignsMonitor());

Managing Imports

In large projects, managing imports can become problematic. Here are some techniques that help keep them organized:

1. Barrel Exports

Index files that re-export all public elements from a given directory:

1// dinosaurMonitoring/index.js
2export * from './vitalSigns';
3export * from './behaviorAnalysis';
4export * from './healthAlerts';
5
6// In another module
7import { checkVitalSigns, analyzeBehavior } from './dinosaurMonitoring';
8// Instead of
9import { checkVitalSigns } from './dinosaurMonitoring/vitalSigns';
10import { analyzeBehavior } from './dinosaurMonitoring/behaviorAnalysis';

2. Import Aliases

Many bundling tools (like webpack, Vite) allow defining aliases for import paths:

1// webpack.config.js
2module.exports = {
3  resolve: {
4    alias: {
5      '@core': path.resolve(__dirname, 'src/core'),
6      '@features': path.resolve(__dirname, 'src/features'),
7      '@shared': path.resolve(__dirname, 'src/shared'),
8    }
9  }
10};
11
12// In code
13import { activateProtocol } from '@core/security';
14import { DinosaurCard } from '@features/dinosaurs/components';

This eliminates the problem of relative paths (../../..) and makes imports more readable.

State Management

In larger applications, state management becomes a key challenge. Here are several approaches:

1. Centralized State Management (Redux, MobX)

Storing the entire application state in one place:

1// Redux actions for dinosaur monitoring system
2const UPDATE_DINOSAUR_HEALTH = 'UPDATE_DINOSAUR_HEALTH';
3const ALERT_HEALTH_ISSUE = 'ALERT_HEALTH_ISSUE';
4
5// Reducer
6function dinosaurHealthReducer(state = initialState, action) {
7  switch (action.type) {
8    case UPDATE_DINOSAUR_HEALTH:
9      return {
10        ...state,
11        dinosaurs: state.dinosaurs.map(dino =>
12          dino.id === action.payload.id
13            ? { ...dino, health: action.payload.health }
14            : dino
15        )
16      };
17    case ALERT_HEALTH_ISSUE:
18      return {
19        ...state,
20        alerts: [...state.alerts, action.payload]
21      };
22    default:
23      return state;
24  }
25}

2. Modularized State (Redux Toolkit, Zustand)

Splitting state into smaller, independent "slices":

1// State slice responsible for dinosaurs
2const dinosaurSlice = createSlice({
3  name: 'dinosaurs',
4  initialState: { list: [], loading: false, error: null },
5  reducers: {
6    fetchStart: (state) => {
7      state.loading = true;
8    },
9    fetchSuccess: (state, action) => {
10      state.list = action.payload;
11      state.loading = false;
12    },
13    updateHealth: (state, action) => {
14      const { id, health } = action.payload;
15      const dinosaur = state.list.find(d => d.id === id);
16      if (dinosaur) dinosaur.health = health;
17    }
18  }
19});
20
21// State slice responsible for enclosures
22const enclosureSlice = createSlice({
23  name: 'enclosures',
24  initialState: { list: [], securityStatus: {} },
25  reducers: {
26    //
27  }
28});

3. Server State vs. Client State

Distinguishing between data coming from the server and UI state:

1// Server data - handled by React Query
2const { data: dinosaurs, isLoading, error } = useQuery(
3  'dinosaurs',
4  () => fetchDinosaurs()
5);
6
7// UI state - handled locally
8const [selectedDinosaurId, setSelectedDinosaurId] = useState(null);

Managing Side Effects

In large applications, managing side effects (API requests, I/O operations, etc.) becomes complex. Here are several approaches:

1. Centralized Management (Redux Saga, Redux Thunk)

1// Redux Saga for dinosaur health monitoring
2function* monitorDinosaurHealthSaga() {
3  while (true) {
4    try {
5      // Fetch all dinosaurs
6      const dinosaurs = yield call(api.fetchAllDinosaurs);
7
8      // Check health of each dinosaur
9      for (const dinosaur of dinosaurs) {
10        const healthData = yield call(api.fetchDinosaurHealth, dinosaur.id);
11
12        // Update state
13        yield put(updateDinosaurHealth(dinosaur.id, healthData));
14
15        // If health is critical, report alert
16        if (healthData.status === 'critical') {
17          yield put(alertHealthIssue(dinosaur.id, healthData));
18        }
19      }
20
21      // Wait 5 minutes before next check
22      yield delay(5 * 60 * 1000);
23    } catch (error) {
24      console.error('Health monitoring error:', error);
25      yield put(monitoringError(error));
26      // Wait 1 minute before retrying
27      yield delay(60 * 1000);
28    }
29  }
30}

2. Effect Hooks (React Query, SWR)

1// React Query hook for managing dinosaur health data
2function useDinosaurHealth(dinosaurId) {
3  return useQuery(
4    ['dinosaurHealth', dinosaurId],
5    () => api.fetchDinosaurHealth(dinosaurId),
6    {
7      // Refresh data every 1 minute
8      refetchInterval: 60 * 1000,
9      // React to health changes
10      onSuccess: (data) => {
11        if (data.status === 'critical') {
12          notifyHealthIssue(dinosaurId, data);
13        }
14      }
15    }
16  );
17}
18
19// In a component
20function DinosaurMonitor({ dinosaurId }) {
21  const { data, isLoading, error } = useDinosaurHealth(dinosaurId);
22
23  if (isLoading) return <Loading />;
24  if (error) return <Error message={error.message} />;
25
26  return (
27    <div className={`health-status ${data.status}`}>
28      <h2>Dinosaur health: {data.status}</h2>
29      <div>Temperature: {data.temperature}°C</div>
30      <div>Heart rate: {data.heartRate} BPM</div>
31      <div>Updated: {formatTime(data.timestamp)}</div>
32    </div>
33  );
34}

Testing Large Applications

In large projects, comprehensive testing is crucial. Here are the levels of testing to consider:

1. Unit Tests

Testing individual modules in isolation:

1// Testing the checkVitalSigns function from dinosaurHealthMonitor
2describe('dinosaurHealthMonitor', () => {
3  describe('checkVitalSigns', () => {
4    it('should correctly identify healthy dinosaurs', () => {
5      const healthyDino = {
6        id: 'trex1',
7        temperature: 38,
8        heartRate: 70,
9        bloodOxygen: 95
10      };
11
12      const result = checkVitalSigns(healthyDino);
13      expect(result.status).toBe('healthy');
14      expect(result.issues).toHaveLength(0);
15    });
16
17    it('should detect high temperature', () => {
18      const feverishDino = {
19        id: 'trex1',
20        temperature: 42, // Too high
21        heartRate: 70,
22        bloodOxygen: 95
23      };
24
25      const result = checkVitalSigns(feverishDino);
26      expect(result.status).toBe('issue');
27      expect(result.issues).toContain('high temperature');
28    });
29  });
30});

2. Integration Tests

Testing cooperation between modules:

1// Testing cooperation between health module and alert module
2describe('Health monitoring integration', () => {
3  it('should generate alerts for health issues', async () => {
4    // Prepare mocks
5    const mockAlertSystem = {
6      sendAlert: jest.fn()
7    };
8
9    const mockDinosaurData = {
10      id: 'raptor2',
11      temperature: 43, // Critically high
12      heartRate: 120,  // Elevated
13      bloodOxygen: 85  // Low oxygen level
14    };
15
16    // Create monitoring system with mocked alert system
17    const healthMonitor = createHealthMonitor(mockAlertSystem);
18
19    // Call monitoring function
20    await healthMonitor.checkAndAlert(mockDinosaurData);
21
22    // Check if alert was sent
23    expect(mockAlertSystem.sendAlert).toHaveBeenCalledWith(
24      expect.objectContaining({
25        dinosaurId: 'raptor2',
26        severity: 'critical',
27        issues: expect.arrayContaining([
28          expect.stringMatching(/temperature/i),
29          expect.stringMatching(/heart rate/i),
30          expect.stringMatching(/oxygen/i)
31        ])
32      })
33    );
34  });
35});

3. End-to-End Tests (E2E)

Testing complete user paths:

1// E2E test of enclosure breach response process
2describe('Enclosure breach response', () => {
3  it('should activate emergency protocol on fence failure', async () => {
4    // Log in as system administrator
5    await login('admin', 'password');
6
7    // Navigate to enclosure management panel
8    await navigateTo('enclosures');
9
10    // Simulate fence failure
11    await simulateFenceFailure('enclosure-b12');
12
13    // Check if alarm was activated
14    const alarmStatus = await getElement('.alarm-status');
15    expect(alarmStatus).toHaveClass('active');
16
17    // Check if emergency protocol was launched
18    const protocolStatus = await getElement('.emergency-protocol');
19    expect(protocolStatus.textContent).toContain('Active');
20
21    // Check if notifications were sent
22    const notifications = await getElements('.notification');
23    expect(notifications).toHaveLength(2); // To security team and management
24
25    // Simulate fence repair
26    await repairFence('enclosure-b12');
27
28    // Check if alarm was deactivated
29    const updatedAlarmStatus = await getElement('.alarm-status');
30    expect(updatedAlarmStatus).not.toHaveClass('active');
31  });
32});

Code Documentation

In large projects, documentation becomes essential. Here are different levels of documentation to consider:

1. Code Documentation (Comments, JSDoc)

1/**
2 * Checks the health status of a dinosaur.
3 * @param {string} dinoId - The unique dinosaur identifier
4 * @param {Object} [options] - Additional options
5 * @param {boolean} [options.detailed=false] - Return detailed report
6 * @returns {Promise<HealthStatus>} Health status object
7 * @throws {DinoNotFoundError} When the dinosaur is not found
8 * @example
9 * const status = await checkHealth('TRX-001', { detailed: true });
10 */
11async function checkHealth(dinoId, options = {}) {
12  const dino = await getDinosaurById(dinoId);
13  if (!dino) throw new DinoNotFoundError(dinoId);
14
15  return {
16    healthy: dino.health > 60,
17    level: dino.health,
18    lastChecked: new Date().toISOString()
19  };
20}

2. Module Documentation (README.md)

Each module should have its own README.md file describing its functionality, interface, and usage:

1# Dinosaur Health Monitoring Module
2
3This module is responsible for monitoring and assessing the health status of dinosaurs in Jurassic Park.
4
5## Features
6
7- Regular checking of dinosaur vital signs
8- Detecting health issues and generating alerts
9- Historical health status tracking and trends
10- Automatic veterinary staff notifications
11
12## API
13
14### `checkVitalSigns(dinosaur)`
15
16Checks dinosaur health based on vital signs.
17
18### `monitorHealth(dinosaurId, interval)`
19
20Starts periodic dinosaur health monitoring.
21
22### `stopMonitoring(dinosaurId)`
23
24Stops dinosaur health monitoring.
25
26## Usage Examples
27
28~~~js
29// One-time health check
30const healthStatus = checkVitalSigns(dinosaur);
31
32// Start continuous monitoring
33const stopMonitoring = monitorHealth('trex1', 60000); // every minute
34
35// Stop monitoring
36stopMonitoring();

Integration with Other Modules

This module uses:

  • Sensor system APIs (src/modules/sensors)
  • Alert system (src/modules/alerts)

Used by:

  • Park management dashboard (src/modules/dashboard)
  • Security system (src/modules/security)
1
2### 3. Architecture Documentation (Diagrams, ADR)
3
4Documenting architectural decisions is crucial for understanding why the system was designed a certain way. Helpful tools include:
5
6- **Architecture diagrams** (C4, UML)
7- **Architecture Decision Records (ADR)** - documents describing important architectural decisions, their context, and consequences
8
9~~~text
10# ADR 001: Choosing Redux as the State Management System
11
12## Context
13
14The Jurassic Park management application needs a state management mechanism that handles:
15- Complex global state (dinosaur data, enclosures, visitors)
16- Asynchronous operations (communication with park systems)
17- State change history tracking (security audit)
18
19## Decision
20
21We decided to use Redux with Redux Toolkit and Redux Saga as the main state management mechanism.
22
23## Rationale
24
25- **Predictability**: Redux ensures unidirectional data flow
26- **Middleware**: Redux Saga simplifies managing complex async operations
27- **DevTools**: Redux developer tools facilitate debugging
28- **Ecosystem**: Rich ecosystem of libraries and tools
29- **Time Travel Debugging**: Ability to track state change history
30
31## Consequences
32
33### Positive
34- Clear data flow in the application
35- Easier debugging of state issues
36- Ability to audit user actions
37
38### Negative
39- Increased amount of code (actions, reducers, selectors)
40- Higher learning curve for new team members
41- Potential performance overhead if used improperly

Best Practices for Large JavaScript Projects

1. Establish Coding Standards

Define and enforce coding standards using tools like ESLint, Prettier:

1// .eslintrc.json
2{
3  "extends": [
4    "eslint:recommended",
5    "plugin:react/recommended"
6  ],
7  "rules": {
8    "react/prop-types": "error",
9    "no-unused-vars": "error",
10    "no-console": ["warn", { "allow": ["warn", "error"] }],
11    "prefer-const": "error"
12  }
13}

2. Use Typing (TypeScript, PropTypes)

1// typescript
2interface DinosaurVitalSigns {
3  id: string;
4  temperature: number;
5  heartRate: number;
6  bloodOxygen: number;
7}
8
9interface HealthAssessment {
10  status: 'healthy' | 'issue' | 'critical';
11  issues: string[];
12  lastChecked: Date;
13}
14
15function checkVitalSigns(dinosaur: DinosaurVitalSigns): HealthAssessment {
16  // implementation...
17}

3. Automate Tests and Deployments (CI/CD)

Set up CI/CD pipelines that automatically run tests, static code analysis, and deploy the application:

1# .github/workflows/ci.yml
2name: CI/CD
3
4on:
5  push:
6    branches: [ main, develop ]
7  pull_request:
8    branches: [ main, develop ]
9
10jobs:
11  test:
12    runs-on: ubuntu-latest
13    steps:
14      - uses: actions/checkout@v2
15      - name: Set up Node.js
16        uses: actions/setup-node@v2
17        with:
18          node-version: '14'
19      - name: Install dependencies
20        run: npm ci
21      - name: Run tests
22        run: npm test
23      - name: Run linting
24        run: npm run lint
25      - name: Build
26        run: npm run build
27
28  deploy:
29    needs: test
30    if: github.ref == 'refs/heads/main'
31    runs-on: ubuntu-latest
32    steps:
33      - uses: actions/checkout@v2
34      # ... deployment steps

4. Use Code Reviews

Establish a Code Review process that ensures code is reviewed by at least one other developer before merging into the main branch.

5. Monitor Performance

Implement application performance monitoring to quickly detect and fix issues:

1// Example custom performance monitoring tool configuration
2const performanceMonitor = {
3  eventTiming: {},
4
5  startTiming(eventName) {
6    this.eventTiming[eventName] = {
7      start: performance.now()
8    };
9  },
10
11  endTiming(eventName) {
12    if (this.eventTiming[eventName]) {
13      const start = this.eventTiming[eventName].start;
14      const duration = performance.now() - start;
15
16      // Log to monitoring server
17      logPerformanceMetric(eventName, duration);
18
19      // Generate alert if operation took too long
20      if (duration > performanceThresholds[eventName]) {
21        alertPerformanceIssue(eventName, duration);
22      }
23
24      delete this.eventTiming[eventName];
25    }
26  }
27};
28
29// Usage
30function loadDinosaurData(id) {
31  performanceMonitor.startTiming('loadDinosaurData');
32
33  return fetchDinosaurData(id)
34    .then(data => {
35      performanceMonitor.endTiming('loadDinosaurData');
36      return data;
37    })
38    .catch(error => {
39      performanceMonitor.endTiming('loadDinosaurData');
40      throw error;
41    });
42}

Summary

Structuring code in larger JavaScript projects is a complex topic requiring a thoughtful approach. Key aspects are:

  1. Folder/file organization — choosing the right structure depending on project size and nature
  2. Application architecture — defining how different parts of the application interact
  3. Code modularization — splitting code into smaller, cohesive modules with clear responsibilities
  4. Import management — maintaining readable and efficient imports
  5. State management — choosing the right approach for managing state
  6. Side effect management — organizing async operations and other side effects
  7. Testing — implementing a comprehensive testing strategy
  8. Documentation — ensuring well-documented code and architecture

By following these principles, you can avoid the chaos that might lead to a "catastrophe" in your JavaScript project — just as proper organization and management would have helped avoid the catastrophe in Jurassic Park!

Go to CodeWorlds