Error Handling & Responses
Effective error handling is essential for maintaining application stability and providing meaningful feedback to API consumers. Instead of manually constructing JSON responses or throwing generic JavaScript errors, Harpia provides a robust, standardised toolset out of the box via the @harpiats/common package: ApiResponse and AppError.
These tools work in symbiosis to automatically format payloads, intercept validation issues, and enforce a consistent contract across your entire architecture.
Standardised API Responses
The ApiResponse class is designed to replace direct calls to res.json() in your controllers, ensuring every response conforms to a predictable envelope.
Successful Responses
When a controller action completes successfully, use ApiResponse.success().
import { ApiResponse } from "@harpiats/common";
import type { Request, Response } from "@harpiats/core";
export const show = async (req: Request, res: Response) => {
const user = await userRepository.findById(req.params.id);
// Automatically wraps the data in { status: "OK", result: ... }
return ApiResponse.success(res, user);
};
This generates the following JSON response:
{
"status": "OK",
"result": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
"error": null
}
You can also pass a string to generate a standard message envelope, or provide a third argument to trigger an HTTP redirect.
Pagination Responses
When returning paginated lists, ApiResponse.pagination() correctly extracts the payload and formats the pagination metadata without boilerplate.
import { ApiResponse, paginate } from "@harpiats/common";
import type { Request, Response } from "@harpiats/core";
export const list = async (req: Request, res: Response) => {
const page = Number(req.query.page) || 1;
const perPage = 10;
// Example fetching data and total count
const data = await userRepository.findMany({ skip: (page - 1) * perPage, take: perPage });
const totalData = await userRepository.count();
// First, structure the data using the paginate helper
const paginatedData = paginate({ data, page, perPage, totalData });
// Finally, send the formatted response
return ApiResponse.pagination(res, paginatedData);
};
This generates the following JSON response with pagination metadata:
{
"status": "OK",
"result": [
{ "id": 1, "name": "John" },
{ "id": 2, "name": "Jane" }
],
"pagination": {
"page": 1,
"lastPage": 5,
"perPage": 10,
"totalPages": 5,
"totalItems": 42
},
"error": null
}
Throwing Business Exceptions
When writing business logic in your Services or Repositories, you should avoid throwing raw new Error(). Instead, use the AppError class.
AppError provides semantic, static factories for common HTTP exceptions, allowing you to explicitly declare the failure state.
// modules/user/services/create.ts
import { AppError } from "@harpiats/common";
export const create = async (email: string) => {
const exists = await userRepository.findByEmail(email);
if (exists) {
// Throws a 400 Bad Request exception dynamically
throw AppError.E_BAD_REQUEST("This email is already in use.");
}
return userRepository.create({ email });
};
When intercepted by the global error handler, this gracefully transforms into an HTTP 400 response:
{
"status": "ERROR",
"error": {
"message": "This email is already in use."
}
}
Available semantic exceptions include:
AppError.E_BAD_REQUEST(message?)AppError.E_UNAUTHORIZED(message?)AppError.E_FORBIDDEN(message?)AppError.E_NOT_FOUND(message?)AppError.E_VALIDATION_FAIL(message?)
The Global Error Handler
You do not need to wrap your controller methods in constant try/catch blocks. Harpia relies on a global error handling middleware that captures any unhandled exception and funnels it directly into ApiResponse.error().
The ApiResponse.error(res, error) method is highly intelligent:
- AppError: If it intercepts an instance of
AppError, it extracts the defined status code and message automatically. - ZodError: If it intercepts a
ZodError(thrown during schema validation), it maps all the issues into an easily readable validation payload:{ "status": "ERROR", "error": { "code": "VALIDATION_ERROR", "issues": { "email": ["Invalid email address format"] } } } - Generic Errors: It checks for standard mapped prefixes (like
E_ROW_NOT_FOUND) and translates them to HTTP404, defaulting all other unknown exceptions to a safe500 Internal Error.
This setup guarantees that your client always receives a structured, safe, and typed JSON response regardless of where or how the error originated within your application lifecycle.