Skip to content

Add standardized error response format (RFC 7807 Problem Details) across all backend routes #172

@Calebux

Description

@Calebux

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 AppError subclasses 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 ValidationError with field-level details

Metadata

Metadata

Labels

BackendStellar WaveIssues in the Stellar wave programdxDeveloper experience

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions