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:
- Router directs request to the appropriate controller.
- Controller validates input and calls relevant service(s).
- Service implements business logic and utilizes model(s).
- 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.
// 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.
// 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.
// 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.
// 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.
// 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;