# Table of Contents - MVCS Overview - Router Layer (The Menu) - Controller Layer (The Waiter) - Service Layer (The Chef) - Model Layer (The Pantry) - Middleware Layer (The Security) ## MVCS Overview The layers work together to handle requests, process data, and return responses: 1. **Router** directs request to the appropriate controller. 2. **Controller** validates input and calls relevant service(s). 3. **Service** implements business logic and utilizes model(s). 4. **Model** interact with the database and represent business entities. # Router Layer (The Menu) Defines the API endpoints of an Express application, mapping incoming clients requests to the appropriate controllers based on the URL and HTTP method. #### Responsibilities - Directs incoming client requests. - Defines routes (endpoints) and corresponding HTTP methods (GET, POST, PUT, etc.). - Associates a path or pattern with a specific controller action. #### Key Concepts - Route Parameters: Captures dynamic values from the URL. - Query Parameters: Handle optional parameters in the URL. - HTTP Methods: Match HTTP verbs to appropriate handlers. - Modular Routing: Group related routes together. - Nested Routes: Handle hierarchical resource relationships. #### Details **Restaurant Analogy**: The dining menu presents the available dishes (endpoints). **Dependencies**: Express frameworks, controllers. **Common mistake**: Implementing business logic, not handling all routes / cases. **iOS Analog:** Routes in a story board or a coordinator class. #### Router Example A PostRouter that has routes for fetching Post(s) and for creating a Post. ```typescript // src/routes/PostRouter.ts import { Router } from 'express'; import PostController from '../controllers/PostController'; class PostRouter { constructor(postController) { this.postController = postController; this.router = Router(); this.initializeRoutes(); } initializeRoutes() { this.router.get('/posts', (req, res) => this.postController.getPosts(req, res)); this.router.get('/posts/:id', (req, res) => this.postController.get(req, res)); this.router.post('/posts', (req, res) => this.postController.create(req, res)); } getRouter() { return this.router; } } export default PostRouter; ``` # Controller Layer (The Waiter) Handles the flow of data between the Model and the View, processing user input and determining how to respond. #### Responsibilities - Handles logic for a single, or closely related set of resources. - Parse, validates and sanitizes input data. - Calls the appropriate service methods based on the request. - Formats and sends back the response (e.g., HTTP status codes, JSON data). #### Key Concepts - Request Parsing: Extract and validate data from request body, params, and query. - Response Formatting: Structure responses consistently (e.g., using a response wrapper). - Error Handling: Catch and process errors from services. - Input Validation: Ensure data integrity before passing to services. #### Details **Restaurant Analogy:** The waiter takes orders (request) and brings back your food (response) as prepared by the kitchen (service layer). **Dependencies:** Services, Views, and Express.js `req` and `res` objects. **Common mistake:** Implementing business logic, too many responsibilities. **iOS analog:** ViewModel in SwiftUI's MVVM pattern. #### Controller Example A PostController that retrieves and creates Posts. ```typescript // PostController.ts import PostService from '../services/PostService'; class PostController { constructor(postService) { this.postService = postService; } async getPostById(req, res) { try { const id = req.params.id; const post = await this.postService.getPostById(id); if (post) { res.json(post); } else { res.status(404).json({ message: 'Post not found' }); } } catch (error) { res.status(500).json({ message: 'Error fetching post', error: error.message }); } } async createPost(req, res) { try { const postData = req.body; const newPost = await this.postService.createPost(postData); res.status(201).json(newPost); } catch (error) { res.status(400).json({ message: 'Error creating post', error: error.message }); } } } export default PostController; ``` # Service Layer (The Chef) Encapsulates the application's business logic and works with the Model layer. #### Responsibilities - Performs the core business logic. - Interacts with the model to retrieve, update, or delete data in the database. #### Key Concepts - Transaction Management: Ensure data consistency across multiple operations. - External Service Integration: Interact with third-party APIs or services. - Caching: Implement caching strategies for improved performance. #### Details **Real-world analogy:** The chef knows how to prepare dishes, combining ingredients and following recipes. **Dependencies:** Models, database connection, external APIs or services. **Common mistake:** Too many responsibilities, directly interacting with HTTP requests/responses. **iOS analog:** Separate Service classes, or sometimes the ViewModel in MVVM. #### Service Example A user service that checks if user credentials are valid and returns either user details or an error. ```typescript // src/services/PostService.ts import { v4 as uuidv4 } from 'uuid'; import { NotFoundError, ValidationError } from '../utils/errors'; class PostService { async getAllPosts(limit = 10, offset = 0) { return Post.findAll({ limit, offset }); } async getPostById(id) { const post = await Post.findByPk(id); if (!post) { throw new NotFoundError('Post not found'); } return post; } async createPost(postData) { this.validatePostData(postData); return Post.create(postData); } validatePostData(postData) { if (!postData.title || postData.title.trim().length === 0) { throw new ValidationError('Post title is required'); } if (!postData.content || postData.content.trim().length === 0) { throw new ValidationError('Post content is required'); } // Add more custom validation as needed } } export default PostService; ``` # Model Layer (The Pantry) Represents the data structures and business entities of the application. #### Responsibilities - Defines the structure of data entities. - Handles data storage and retrieval through ORM/ODM libraries or database queries. #### Key Concepts - Schema Definition: Define the structure and constraints of data entities. - Data Validation: Ensure data integrity at the database level. - Query Interface: Provide methods for complex database queries. - Lifecycle Hooks: Implement pre/post save, update, delete operations. #### Details **Restaurant Analogy**: The ingredients and their properties. **Dependencies**: Ideally, models should have minimal dependencies: database, data validation, or date/time libraries. **Common mistakes**: Depending on other layers, implementing business logic (if Service layer exists), tight coupling to a specific database. **iOS Analog**: Core Data entities or Swift struct / classe models. #### Model Example Defining a Post model and the Sequelize schema. ```typescript // src/models/Post.ts import { Model, DataTypes } from 'sequelize'; import sequelize from '../config/database'; class Post extends Model { public id!: string; public title!: string; public content!: string; public readonly createdAt!: Date; public readonly updatedAt!: Date; } Post.init( { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true, }, title: { type: DataTypes.STRING, allowNull: false, validate: { notEmpty: { msg: "Title cannot be empty" }, len: { args: [1, 255], msg: "Title must be 1 - 255 characters" } } }, content: { type: DataTypes.TEXT, allowNull: false, validate: { notEmpty: { msg: "Content cannot be empty" } } }, }, { sequelize, modelName: 'Post', } ); export { Post }; ``` ---- # Middleware Layer (The Security) Inspects and modifies request/response objects before the request hits your routes or before the response is returned to the client. #### Responsibilities - Executes code before and/or after route handlers - Modifies request / response objects - Cross-cutting concerns: logging, error handling, authentication, etc. #### Key Concepts - Middleware Chain: Middleware functions are executed sequentially. The `next()` function is crucial for passing control to the next middleware. - Built-in vs Custom: Express has built-in middleware (e.g. `express.json()`), but you often need to write custom middleware. - Application-level vs Route-level: Can be applied to all routes or to specific routes. - Async Middleware: When using async operations, always catch errors and pass them to `next(error)`. #### Details **Restaurant Analogy**: Inspects and prepares guests (requests/response) prior to entry & exit. **Dependencies**: Express.js, relevant libraries (e.g. body-parser, cors, helmet). **Common mistakes**: Overusing middleware, implementing business logic, not handling errors properly in async middleware. **iOS Analog**: Delegates that allow for additional processing/handling between layers. #### Middleware Example An authentication middleware that verifies JWT tokens. ```typescript // src/middleware/authMiddleware.ts import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; const authMiddleware = (req: Request, res: Response, next: NextFunction) => { const token = req.header('Authorization')?.replace('Bearer ', ''); if (!token) { return res.status(401).json({ error: 'No token provided' }); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET as string); req.user = decoded; next(); } catch (error) { res.status(401).json({ error: 'Invalid token' }); } }; export default authMiddleware; ```