# 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;
```