With your models and migrations in place, it’s time to implement the actual CRUD (Create, Read, Update, Delete) operations. This part focuses on building a clean architecture with controllers and services that handle database operations efficiently. We’ll also explore eager loading to optimize queries and retrieve related data.
Architecture Overview
A clean architecture separates concerns into distinct layers. Requests flow through routes to controllers, which delegate business logic to services. Services interact with models to perform database operations. This separation makes your code maintainable, testable, and scalable.
Creating Service Layer
Create src/services/userService.js to handle all user-related database operations:
const { User, Post } = require('../models');
class UserService {
async createUser(userData) {
try {
const user = await User.create({
firstName: userData.firstName,
lastName: userData.lastName,
email: userData.email,
password: userData.password,
});
return user;
} catch (error) {
throw new Error(`Failed to create user: ${error.message}`);
}
}
async getUserById(userId) {
try {
const user = await User.findByPk(userId, {
include: {
model: Post,
as: 'posts',
attributes: ['id', 'title', 'content', 'createdAt'],
},
});
if (!user) {
throw new Error('User not found');
}
return user;
} catch (error) {
throw new Error(`Failed to fetch user: ${error.message}`);
}
}
async getAllUsers(limit = 10, offset = 0) {
try {
const users = await User.findAll({
attributes: { exclude: ['password'] },
include: {
model: Post,
as: 'posts',
attributes: ['id', 'title'],
},
limit,
offset,
order: [['createdAt', 'DESC']],
});
return users;
} catch (error) {
throw new Error(`Failed to fetch users: ${error.message}`);
}
}
async updateUser(userId, updateData) {
try {
const user = await User.findByPk(userId);
if (!user) {
throw new Error('User not found');
}
await user.update(updateData);
return user;
} catch (error) {
throw new Error(`Failed to update user: ${error.message}`);
}
}
async deleteUser(userId) {
try {
const user = await User.findByPk(userId);
if (!user) {
throw new Error('User not found');
}
await user.destroy();
return { message: 'User deleted successfully' };
} catch (error) {
throw new Error(`Failed to delete user: ${error.message}`);
}
}
}
module.exports = new UserService();
Now create src/services/postService.js for post operations:
const { Post, User } = require('../models');
class PostService {
async createPost(postData) {
try {
const post = await Post.create({
title: postData.title,
content: postData.content,
userId: postData.userId,
});
return post;
} catch (error) {
throw new Error(`Failed to create post: ${error.message}`);
}
}
async getPostById(postId) {
try {
const post = await Post.findByPk(postId, {
include: {
model: User,
as: 'author',
attributes: ['id', 'firstName', 'lastName', 'email'],
},
});
if (!post) {
throw new Error('Post not found');
}
return post;
} catch (error) {
throw new Error(`Failed to fetch post: ${error.message}`);
}
}
async getAllPosts(limit = 10, offset = 0) {
try {
const posts = await Post.findAll({
include: {
model: User,
as: 'author',
attributes: ['id', 'firstName', 'lastName'],
},
limit,
offset,
order: [['createdAt', 'DESC']],
});
return posts;
} catch (error) {
throw new Error(`Failed to fetch posts: ${error.message}`);
}
}
async getPostsByUserId(userId, limit = 10, offset = 0) {
try {
const posts = await Post.findAll({
where: { userId },
limit,
offset,
order: [['createdAt', 'DESC']],
});
return posts;
} catch (error) {
throw new Error(`Failed to fetch user posts: ${error.message}`);
}
}
async updatePost(postId, updateData) {
try {
const post = await Post.findByPk(postId);
if (!post) {
throw new Error('Post not found');
}
await post.update(updateData);
return post;
} catch (error) {
throw new Error(`Failed to update post: ${error.message}`);
}
}
async deletePost(postId) {
try {
const post = await Post.findByPk(postId);
if (!post) {
throw new Error('Post not found');
}
await post.destroy();
return { message: 'Post deleted successfully' };
} catch (error) {
throw new Error(`Failed to delete post: ${error.message}`);
}
}
}
module.exports = new PostService();
Creating Controller Layer
Create src/controllers/userController.js to handle HTTP requests:
const userService = require('../services/userService');
class UserController {
async create(req, res) {
try {
const user = await userService.createUser(req.body);
res.status(201).json({
success: true,
message: 'User created successfully',
data: user,
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message,
});
}
}
async getById(req, res) {
try {
const user = await userService.getUserById(req.params.id);
res.status(200).json({
success: true,
data: user,
});
} catch (error) {
res.status(404).json({
success: false,
message: error.message,
});
}
}
async getAll(req, res) {
try {
const limit = req.query.limit || 10;
const offset = req.query.offset || 0;
const users = await userService.getAllUsers(limit, offset);
res.status(200).json({
success: true,
data: users,
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message,
});
}
}
async update(req, res) {
try {
const user = await userService.updateUser(req.params.id, req.body);
res.status(200).json({
success: true,
message: 'User updated successfully',
data: user,
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message,
});
}
}
async delete(req, res) {
try {
const result = await userService.deleteUser(req.params.id);
res.status(200).json({
success: true,
message: result.message,
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message,
});
}
}
}
module.exports = new UserController();
Create src/controllers/postController.js:
const postService = require('../services/postService');
class PostController {
async create(req, res) {
try {
const post = await postService.createPost(req.body);
res.status(201).json({
success: true,
message: 'Post created successfully',
data: post,
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message,
});
}
}
async getById(req, res) {
try {
const post = await postService.getPostById(req.params.id);
res.status(200).json({
success: true,
data: post,
});
} catch (error) {
res.status(404).json({
success: false,
message: error.message,
});
}
}
async getAll(req, res) {
try {
const limit = req.query.limit || 10;
const offset = req.query.offset || 0;
const posts = await postService.getAllPosts(limit, offset);
res.status(200).json({
success: true,
data: posts,
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message,
});
}
}
async getByUserId(req, res) {
try {
const limit = req.query.limit || 10;
const offset = req.query.offset || 0;
const posts = await postService.getPostsByUserId(
req.params.userId,
limit,
offset
);
res.status(200).json({
success: true,
data: posts,
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message,
});
}
}
async update(req, res) {
try {
const post = await postService.updatePost(req.params.id, req.body);
res.status(200).json({
success: true,
message: 'Post updated successfully',
data: post,
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message,
});
}
}
async delete(req, res) {
try {
const result = await postService.deletePost(req.params.id);
res.status(200).json({
success: true,
message: result.message,
});
} catch (error) {
res.status(400).json({
success: false,
message: error.message,
});
}
}
}
module.exports = new PostController();
Creating Routes
Create src/routes/userRoutes.js:
const express = require('express');
const userController = require('../controllers/userController');
const router = express.Router();
router.post('/', userController.create);
router.get('/', userController.getAll);
router.get('/:id', userController.getById);
router.put('/:id', userController.update);
router.delete('/:id', userController.delete);
module.exports = router;
Create src/routes/postRoutes.js:
const express = require('express');
const postController = require('../controllers/postController');
const router = express.Router();
router.post('/', postController.create);
router.get('/', postController.getAll);
router.get('/:id', postController.getById);
router.get('/user/:userId', postController.getByUserId);
router.put('/:id', postController.update);
router.delete('/:id', postController.delete);
module.exports = router;
Updating Main Application File
Update src/index.js to include the routes:
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const sequelize = require('./config/sequelize');
const userRoutes = require('./routes/userRoutes');
const postRoutes = require('./routes/postRoutes');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(helmet());
app.use(cors());
app.use(express.json());
app.get('/health', (req, res) => {
res.json({ status: 'Server is running' });
});
app.use('/api/users', userRoutes);
app.use('/api/posts', postRoutes);
const startServer = async () => {
try {
await sequelize.authenticate();
await sequelize.sync({ alter: false });
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
};
startServer();
Understanding Eager Loading
Notice the ‘include’ option in our service methods. This is eager loading, which retrieves associated data in a single query instead of multiple queries. Without eager loading, fetching a user would require a separate query for each user’s posts, causing the N+1 problem.
// With eager loading (efficient)
const user = await User.findByPk(userId, {
include: { model: Post, as: 'posts' }
});
// Without eager loading (inefficient - N+1 problem)
const user = await User.findByPk(userId);
const posts = await Post.findAll({ where: { userId } });
API Endpoints Reference
User Endpoints:
- POST /api/users – Create new user
- GET /api/users – Get all users with pagination
- GET /api/users/:id – Get user by ID with posts
- PUT /api/users/:id – Update user
- DELETE /api/users/:id – Delete user
Post Endpoints:
- POST /api/posts – Create new post
- GET /api/posts – Get all posts with pagination
- GET /api/posts/:id – Get post by ID with author
- GET /api/posts/user/:userId – Get posts by user
- PUT /api/posts/:id – Update post
- DELETE /api/posts/:id – Delete post
Testing CRUD Operations
Test creating a user with curl:
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"password": "securepassword123"
}'
Test creating a post:
curl -X POST http://localhost:3000/api/posts \
-H "Content-Type: application/json" \
-d '{
"title": "My First Post",
"content": "This is the content of my first post",
"userId": 1
}'
Fetch user with posts:
curl http://localhost:3000/api/users/1
Request Response Flow Visualization
sequenceDiagram participant Client participant Route participant Controller participant Service participant Model participant DB as PostgreSQL Client->>Route: POST /api/users Route->>Controller: userController.create() Controller->>Service: userService.createUser() Service->>Model: User.create() Model->>DB: INSERT INTO Users DB-->>Model: User record created Model-->>Service: User instance Service-->>Controller: User object Controller-->>Client: JSON response
Query Optimization Tips
- Use eager loading with ‘include’ to avoid N+1 queries
- Select only needed attributes to reduce data transfer
- Use ‘where’ clause to filter data at database level
- Implement pagination with limit and offset for large datasets
- Use database indexes on frequently queried columns
Common CRUD Mistakes
Returning passwords: Always exclude passwords from responses. We used attributes: {exclude: [‘password’]} in getAllUsers.
Missing error handling: Always wrap database operations in try-catch blocks to handle failures gracefully.
N+1 queries: Always use eager loading when you need related data to avoid multiple queries.
What’s Next
In Part 4, we’ll explore advanced production-ready patterns including error handling middleware, input validation, transactions, connection pooling optimization, and performance monitoring. We’ll also discuss best practices for deploying your Sequelize application.
References
- Sequelize Querying: https://sequelize.org/docs/v6/core-concepts/model-querying-basics/
- Sequelize Eager Loading: https://sequelize.org/docs/v6/core-concepts/assocs/#eager-loading
- REST API Best Practices: https://restfulapi.net/
- Node.js Error Handling: https://nodejs.org/en/docs/guides/error-handling/