-
Notifications
You must be signed in to change notification settings - Fork 57
Add standardized error response format (RFC 7807 Problem Details) across all backend routes #172
Copy link
Copy link
Open
Labels
BackendStellar WaveIssues in the Stellar wave programIssues in the Stellar wave programdxDeveloper experienceDeveloper experience
Description
Description
Backend error responses are inconsistent across routes. Some return { error: 'message' }, others return { message: 'error' }, others return raw Express errors. The SDK and client have to handle different error shapes depending on which endpoint failed.
Current State (inconsistent)
// Route A
{ "error": "Subscription not found" }
// Route B
{ "message": "Unauthorized", "status": 401 }
// Route C (unhandled Express error)
{ "message": "Internal server error" }Solution: RFC 7807 Problem Details
{
"type": "https://syncro.app/errors/not-found",
"title": "Subscription Not Found",
"status": 404,
"detail": "No subscription with ID 'abc-123' exists for this user.",
"instance": "/api/v1/subscriptions/abc-123",
"requestId": "550e8400-e29b-41d4-a716"
}Implementation
Error classes
// /backend/src/errors/index.ts
export class AppError extends Error {
constructor(
public title: string,
public status: number,
public detail: string,
public type: string = 'about:blank'
) { super(detail); }
}
export class NotFoundError extends AppError {
constructor(detail: string) {
super('Not Found', 404, detail, 'https://syncro.app/errors/not-found');
}
}
export class ValidationError extends AppError {
constructor(detail: string, public errors?: Record<string, string[]>) {
super('Validation Error', 422, detail, 'https://syncro.app/errors/validation');
}
}
export class UnauthorizedError extends AppError {
constructor() {
super('Unauthorized', 401, 'Authentication required.', 'https://syncro.app/errors/unauthorized');
}
}Global error handler middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const requestId = res.getHeader('x-request-id') as string;
if (err instanceof AppError) {
return res.status(err.status).json({
type: err.type,
title: err.title,
status: err.status,
detail: err.detail,
instance: req.path,
requestId,
});
}
// Unexpected error — don't leak internals
logger.error('Unhandled error', { err, requestId });
res.status(500).json({
type: 'https://syncro.app/errors/internal',
title: 'Internal Server Error',
status: 500,
detail: 'An unexpected error occurred.',
instance: req.path,
requestId,
});
});Acceptance Criteria
- All custom error classes defined
- Global error handler middleware installed
- All routes throw
AppErrorsubclasses instead of generic errors - Error shape documented in OpenAPI spec
- SDK parses Problem Details format and throws typed error classes
- Zod validation errors formatted as
ValidationErrorwith field-level details
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
BackendStellar WaveIssues in the Stellar wave programIssues in the Stellar wave programdxDeveloper experienceDeveloper experience