JavaScript and TypeScript course Β· Module 5: Advanced JavaScript
Structuring Code in a Larger Project
In this lesson11
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:
- Maintainability - easier to find and fix bugs
- Scalability - ability to add new features without breaking existing ones
- Testability - ability to test components in isolation
- Collaboration ease - multiple developers can work in parallel on different parts
- 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.json2. 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.json3. 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 improperlyBest 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 steps4. 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:
- Folder/file organization - choosing the right structure depending on project size and nature
- Application architecture - defining how different parts of the application interact
- Code modularization - splitting code into smaller, cohesive modules with clear responsibilities
- Import management - maintaining readable and efficient imports
- State management - choosing the right approach for managing state
- Side effect management - organizing async operations and other side effects
- Testing - implementing a comprehensive testing strategy
- 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!
Code for this lesson: main.js
1// ES Modules in the browser - Jurassic Park
2console.log("ES Modules in the Browser\n");
3
4// Demo of <script type="module">
5console.log("=== HTML Example ===\n");
6console.log(`<!DOCTYPE html>
7<html>
8<head>
9 <title>Jurassic Park</title>
10</head>
11<body>
12 <!-- ES Module in the browser -->
13 <script type="module">
14 import { Dinosaur } from './dinosaur.js';
15
16 const rex = new Dinosaur('Rex');
17 console.log(rex);
18 </script>
19
20 <!-- Fallback for old browsers -->
21 <script nomodule>
22 console.log('Your browser does not support ES Modules');
23 </script>
24</body>
25</html>`);
26
27// Dynamic import in the browser
28console.log("\n=== Dynamic Import ===\n");
29
30async function loadDinosaurModule() {
31 try {
32 console.log("β³ Loading module...");
33
34 // Simulated dynamic import
35 const module = {
36 Dinosaur: class {
37 constructor(name) {
38 this.name = name;
39 }
40 }
41 };
42
43 console.log("Module loaded!");
44 const dino = new module.Dinosaur('Blue');
45 console.log("Created:", dino);
46
47 } catch (error) {
48 console.error("Loading error:", error);
49 }
50}
51
52loadDinosaurModule();
53
54// Import maps
55console.log("\n=== Import Maps ===\n");
56console.log(`<script type="importmap">
57{
58 "imports": {
59 "dinosaurs": "/modules/dinosaurs.js",
60 "security": "/modules/security.js",
61 "lodash": "https://cdn.skypack.dev/lodash"
62 }
63}
64</script>
65
66<script type="module">
67 // Now we can use aliases
68 import { Dinosaur } from 'dinosaurs';
69 import { Security } from 'security';
70 import _ from 'lodash';
71</script>`);
72
73console.log("\nES Modules in the browser:");
74console.log("Native support (no bundler)");
75console.log("Automatic defer");
76console.log("Strict mode by default");
77console.log("Import maps for aliases");
78
79console.log("\nModern browsers = native modules!");Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is the difference between type-based and feature-based project organization?
Hands-on tasks in the game
- Code editor
Create a modular structure with separation of concerns and a clear API for each module.