Back to Full-Stack Mastery

Module 6: Architecture & Best Practices

Master software architecture patterns, clean code principles, and production-ready development practices

📚 Clean Code Principles

1. Meaningful Names

// Bad

const d = new Date();

const x = getUserData();

// Good

const currentDate = new Date();

const activeUser = getUserData();

2. Single Responsibility Principle

Each function/class should do one thing well.

// Bad - doing too much

function processUser(user) {

validateUser(user);

saveToDatabase(user);

sendWelcomeEmail(user);

logActivity(user);

}

// Good - separated concerns

function createUser(user) {

const validated = validateUser(user);

const saved = await userRepository.save(validated);

await emailService.sendWelcome(saved);

await activityLogger.log('user_created', saved.id);

return saved;

}

3. DRY (Don't Repeat Yourself)

// Bad - repetition

const userFullName = user.firstName + ' ' + user.lastName;

const adminFullName = admin.firstName + ' ' + admin.lastName;

// Good - reusable function

function getFullName(person) {

return `${person.firstName} ${person.lastName} `;

}

4. Keep Functions Small

Functions should be 20 lines or less, do one thing, and have clear names.

5. Error Handling

// Use try-catch for async operations

async function fetchUserData(id) {

try {

const user = await api.getUser(id);

return user;

} catch (error) {

logger.error('Failed to fetch user', { id, error });

throw new UserNotFoundError(id);

}

}

🏗️ Design Patterns

MVC (Model-View-Controller)

Separate data, presentation, and logic.

// Model - Data layer

class UserModel {

async findById(id) { /* database query */ }

async create(data) { /* insert */ }

}

// Controller - Business logic

class UserController {

async getUser(req, res) {

const user = await UserModel.findById(req.params.id);

res.json(user);

}

}

// View - Presentation (React component)

function UserProfile({ user }) {

return <div> {user.name} </div> ;

}

Repository Pattern

Abstract data access logic.

class UserRepository {

constructor(database) {

this.db = database;

}

async findAll() {

return this.db.query('SELECT * FROM users');

}

async findById(id) {

return this.db.query('SELECT * FROM users WHERE id = ?', [id]);

}

async create(user) {

return this.db.query('INSERT INTO users SET ?', user);

}

}

Dependency Injection

Pass dependencies instead of creating them internally.

// Bad - tight coupling

class UserService {

constructor() {

this.repo = new UserRepository();

}

}

// Good - dependency injection

class UserService {

constructor(userRepository) {

this.repo = userRepository;

}

}

// Usage

const repo = new UserRepository(database);

const service = new UserService(repo);

📦 Monorepo with Turborepo

Manage multiple packages in a single repository for better code sharing and consistency.

# Project structure

my-monorepo/

├── apps/

│ ├── web/ # Next.js frontend

│ ├── api/ # Express backend

│ └── admin/ # Admin dashboard

├── packages/

│ ├── ui/ # Shared UI components

│ ├── config/ # Shared configs

│ ├── types/ # Shared TypeScript types

│ └── utils/ # Shared utilities

├── turbo.json

└── package.json

# turbo.json

{

"pipeline": {

"build": {

"dependsOn": ["^build"],

"outputs": [".next/**", "dist/**"]

},

"dev": {

"cache": false

}

}

}

⚡ Performance Optimization

Frontend Optimization

  • • Code splitting and lazy loading
  • • Image optimization (Next.js Image component)
  • • Minimize bundle size (tree shaking)
  • • Use React.memo for expensive components
  • • Implement virtual scrolling for long lists
  • • Optimize fonts (font-display: swap)

Backend Optimization

  • • Database query optimization and indexing
  • • Implement caching (Redis)
  • • Use connection pooling
  • • Compress responses (gzip)
  • • Rate limiting to prevent abuse
  • • Async processing for heavy tasks

Caching Strategy

const redis = require('redis');

const client = redis.createClient();

async function getCachedUser(id) {

// Try cache first

const cached = await client.get(`user:${id} `);

if (cached) return JSON.parse(cached);

// Fetch from database

const user = await db.users.findById(id);

// Cache for 1 hour

await client.setEx(`user:${id} `, 3600, JSON.stringify(user));

return user;

}

🚨 Error Handling Strategies

Custom Error Classes

class AppError extends Error {

constructor(message, statusCode) {

super(message);

this.statusCode = statusCode;

this.isOperational = true;

}

}

class NotFoundError extends AppError {

constructor(resource) {

super(`${resource} not found`, 404);

}

}

class ValidationError extends AppError {

constructor(message) {

super(message, 400);

}

}

Global Error Handler

app.use((err, req, res, next) => {

// Log error

logger.error(err);

// Send to error tracking (Sentry)

if (process.env.NODE_ENV =>= 'production') {

Sentry.captureException(err);

}

// Send response

res.status(err.statusCode || 500).json({

error: {

message: err.message,

...(process.env.NODE_ENV =>= 'development' && { stack: err.stack })

}

});

});

📝 Logging Best Practices

const winston = require('winston');

const logger = winston.createLogger({

level: process.env.LOG_LEVEL || 'info',

format: winston.format.combine(

winston.format.timestamp(),

winston.format.json()

),

transports: [

new winston.transports.Console(),

new winston.transports.File({ filename: 'error.log', level: 'error' }),

new winston.transports.File({ filename: 'combined.log' })

]

});

// Usage

logger.info('User logged in', { userId: user.id });

logger.error('Database connection failed', { error });

logger.warn('Rate limit exceeded', { ip: req.ip });

What to Log

  • ✓ Authentication events (login, logout, failed attempts)
  • ✓ API requests (method, path, status, duration)
  • ✓ Errors and exceptions with stack traces
  • ✓ Database queries (in development)
  • ✓ Important business events
  • ✗ Sensitive data (passwords, tokens, PII)

👀 Code Review Best Practices

What to Look For

  • • Code follows project conventions and style guide
  • • Logic is clear and easy to understand
  • • Edge cases are handled
  • • Tests are included and passing
  • • No security vulnerabilities
  • • Performance implications considered
  • • Documentation is updated

Review Etiquette

  • • Be kind and constructive
  • • Ask questions instead of making demands
  • • Praise good solutions
  • • Suggest alternatives with reasoning
  • • Focus on the code, not the person
  • • Respond promptly to reviews

🎯 Module Summary

You've learned production-ready architecture and best practices:

  • ✓ Clean code principles (SOLID, DRY, KISS)
  • ✓ Design patterns (MVC, Repository, Dependency Injection)
  • ✓ Monorepo architecture with Turborepo
  • ✓ Performance optimization strategies
  • ✓ Error handling and logging
  • ✓ Code review best practices

Next: Build a complete SaaS application in Module 7!