Sequelize + Express Validation & Error Handling: Build Production-Ready Node.js APIs
You built your CRUD endpoints.
Your requests return 200.
Your database saves data.
Everything works.
But here’s the real question:
👉 Is your API ready for production?
In this post, we’ll take our Sequelize + Express project to the next level by adding:
- ✅ Proper model validation
- ✅ Centralized error handling
- ✅ Clean, meaningful HTTP responses
- ✅ Resilience against real-world failures
If you’re building APIs with Node.js, this is a must-have upgrade.
🚀 Complete JavaScript Guide (Beginner + Advanced)
🚀 NodeJS – The Complete Guide (MVC, REST APIs, GraphQL, Deno)
The Problem: Trusting the API Consumer
Earlier, we implemented full CRUD for a User model:
- Create user
- Get user(s)
- Update user
- Delete user
It worked perfectly.
But there was one big issue:
We trusted the client to send valid name and email.
In real life, that assumption will break your system.
Users send:
- Empty strings
- Invalid emails
- Missing fields
- Unexpected values
Your API must defend itself.
Step 1: Model-Level Validation in Sequelize
Validation starts when designing your model.
Let’s look at our User model.
Validating the name Field
What do we want?
- The name must exist
- It must not be empty
- It shouldn’t be extremely long
- It must respect the database column limit
Since Sequelize created a VARCHAR(255) field in MySQL, we should enforce a stricter limit at the application level.
Example:
@AllowNull(false)
@Column({
validate: {
len: [1, 100]
}
})
name!: string;
What this does:
@AllowNull(false)→ Prevents null valueslen: [1, 100]→ Enforces minimum and maximum length
Now invalid names never reach the database.
Validating the email Field
For email, we want:
- Required
- Unique
- Proper email format
Example:
@AllowNull(false)
@IsEmail
@Column({
unique: true
})
email!: string;
Now Sequelize automatically:
- Rejects missing emails
- Rejects invalid formats
- Prevents duplicate emails
Model validation is your first line of defense.
But we’re only halfway done.
Step 2: Centralized Error Handling in Express
Validation is great.
But what happens when it fails?
Many developers use try/catch inside every route. That works — but it doesn’t scale.
If your app has:
- Multiple models
- Multiple services
- Multiple validations
You’ll end up duplicating error logic everywhere.
There’s a better way.
Use Express Error Middleware
Express has built-in support for centralized error handling.
Instead of catching errors in every route, let them bubble up.
Then handle them in one place.
Creating a Custom Error Handler
Create:
src/middleware/errorHandler.ts
A typical error handler looks like this:
export default function errorHandler(
error: unknown,
req: Request,
res: Response,
next: NextFunction
) {
if (res.headersSent) {
return next(error);
} // custom logic here
}
Important:
We first check res.headersSent.
If headers were already sent, we pass the error to Express’ default handler.
This prevents double responses.
Step 3: Handling Sequelize Validation Errors
Sequelize throws a ValidationError when model validation fails.
We can detect it like this:
if (error instanceof ValidationError) {
return res.status(422).json({
error: "Validation error",
details: error.errors
});
}
Why 422?
422 Unprocessable Entity means:
The request was syntactically correct, but semantically invalid.
Perfect for validation failures.
Step 4: Safe Error Message Extraction
In JavaScript, anything can be thrown.
In TypeScript, errors are typed as unknown.
So we create a utility:
export function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
} if (typeof error === "string") {
return error;
} return "An error occurred";
}
Now we can safely return error messages without crashing the app.
Step 5: Handling Database Connection Errors
When the database is unavailable, Sequelize throws a ConnectionError.
Instead of exposing internal details, we return:
if (error instanceof ConnectionError) {
return res.status(503).json({
error: "Database unavailable"
});
}
Why 503?
503 Service Unavailable means:
The server is temporarily unable to handle the request.
This is perfect when your DB is down.
Step 6: Handling Database-Level Errors
Sequelize also throws DatabaseError for:
- Invalid columns
- SQL syntax errors
- Constraint violations
Example:
if (error instanceof DatabaseError) {
return res.status(500).json({
error: getErrorMessage(error)
});
}
These are server-side problems.
4xx vs 5xx: Know the Difference
Understanding status codes is critical.
4xx → Client Mistake
Examples:
- 400 Bad Request
- 401 Unauthorized
- 404 Not Found
- 422 Validation Error
The client sent something wrong.
5xx → Server Problem
Examples:
- 500 Internal Server Error
- 503 Service Unavailable
Something failed on your side.
Your API consumers depend on these signals.
Step 7: Register the Error Middleware
This is crucial.
In server.ts, the error handler must be the last middleware:
app.use(errorHandler);
Always register it after all routes.
Testing the Improvements
Try these scenarios:
- Create user without
name - Create user with empty
name - Use invalid email format
- Stop the database
- Request invalid column
Now instead of crashes or messy stack traces, you get:
- Structured JSON errors
- Proper HTTP status codes
- Clean, professional responses
What We Achieved
After implementing validation + centralized error handling:
✅ Bad data is rejected early
✅ Errors are meaningful and structured
✅ Sensitive stack traces are hidden
✅ The API behaves predictably
✅ Database failures are handled gracefully
This is how you move from:
“It works on my machine”
to
“It’s production-ready.”
What’s Next?
There’s still one missing piece.
How do you keep your database structure consistent across:
- Local development
- Staging
- Production
- Multiple team members
The answer:
👉 Sequelize Migrations