Welcome to the Jose Madrid Salsa developer docs — explore features, APIs, and deployment guides.
Jose Madrid SalsaJMS Docs

Error Responses

API error response format, common error codes, and error handling patterns

Error Responses

All API endpoints return errors in a consistent JSON format. This page documents the error envelope, status codes, and common error scenarios you will encounter.

Error Response Format

Every error response follows this structure:

Error envelope
{
  "error": "Human-readable error message",
  "details": {
    // Optional — present for validation errors
  }
}
FieldTypeDescription
errorstringA human-readable description of what went wrong
detailsobject | undefinedAdditional context, typically Zod validation errors. Only present on 400 responses with validation failures

Success Responses

Success responses do not include an error field. They return the data directly as the JSON body, with no wrapping envelope. Paginated responses include a pagination object alongside the data array.

HTTP Status Codes

Client Errors (4xx)

StatusNameWhen It Occurs
400Bad RequestInvalid request body, missing required fields, or validation failure
401UnauthorizedNo valid session token, or session has expired
402Payment RequiredCredit card declined or payment processing failure (Stripe)
403ForbiddenAuthenticated but lacking the required role or permission
404Not FoundRequested resource does not exist
409ConflictDuplicate resource (e.g., registering with an existing email, creating a product with a duplicate SKU)
429Too Many RequestsRate limit exceeded. See Rate Limiting

Server Errors (5xx)

StatusNameWhen It Occurs
500Internal Server ErrorUnhandled exception, database error, or external service failure

Common Error Scenarios

401 Unauthorized

Returned when a request lacks a valid session or the session has expired.

Missing session
{
  "error": "Unauthorized - authentication required"
}

Common causes:

  • No next-auth.session-token cookie present
  • Session token has expired (sessions last 30 days)
  • Cookie was not included in the request headers

Resolution: Sign in again via /api/auth/callback/credentials or an OAuth provider to obtain a fresh session token.

403 Forbidden

Returned when the user is authenticated but does not have the required role or permission.

Insufficient role
{
  "error": "Forbidden - requires one of: ADMIN"
}
Insufficient permission
{
  "error": "Forbidden - requires permission: products:write"
}

Common causes:

  • A CUSTOMER user attempting to access an admin endpoint
  • A STAFF user attempting an action that requires ADMIN role
  • Accessing another user's resource (orders, cart items)

Resolution: Ensure the user has the appropriate role or permission. See Authentication - RBAC for the full permission matrix.

400 Bad Request (Validation)

Returned when request data fails Zod schema validation. Includes a details object with field-level errors.

Validation error (registration)
{
  "error": "Invalid registration data.",
  "details": {
    "fieldErrors": {
      "email": ["Invalid email"],
      "password": ["String must contain at least 8 character(s)"]
    },
    "formErrors": []
  }
}
Missing required fields
{
  "error": "Missing required fields: name, slug, sku, price"
}

Common causes:

  • Missing required fields in the request body
  • Invalid field types (string where number expected)
  • Values outside allowed ranges (e.g., password too short)

Resolution: Check the details.fieldErrors object for specific field issues and correct your request payload.

409 Conflict

Returned when a request would create a duplicate resource.

Duplicate email
{
  "error": "An account with this email already exists."
}
Duplicate SKU
{
  "error": "A product with this SKU already exists"
}

400 Bad Request (Business Rule)

Returned when a request violates a business rule.

Invalid bulk operation
{
  "error": "productIds must be a non-empty array"
}
Already paid
{
  "error": "Order already paid"
}

402 Payment Required

Returned when a payment is declined by Stripe.

Card declined
{
  "error": "Your card was declined."
}

Error Handling Helpers

The API uses a set of helper functions from lib/api.ts to generate consistent error responses.

HelperDefault StatusDefault Message
fail(message, status?, details?)400(required)
unauthorized(message?)401"Unauthorized"
forbidden(message?)403"Forbidden"
notFound(message?)404"Not found"
serverError(message?, error?)500"Internal server error"

tryCatch Wrapper

The tryCatch utility function automatically maps thrown error messages to the appropriate status code:

  • Errors containing "Unauthorized" return 401
  • Errors containing "Forbidden" return 403
  • Errors containing "Not found" return 404
  • All other errors return 500

This means requireRole and requirePermission from the RBAC module throw errors with these keywords, which tryCatch translates into the correct HTTP responses.

Error Messages in Production

Error messages from 500 responses use a generic message and do not expose internal details. The actual error is logged server-side. Never rely on 500 error message text for client-side logic.

Rate Limit Errors

Rate limit exceeded errors include a retryAfter field indicating when the client can retry:

429 response
{
  "error": "Rate limit exceeded",
  "retryAfter": 45
}

The retryAfter value is in seconds. See Rate Limiting for full details on limits and response headers.

Best Practices for Error Handling

Check the Status Code First

Use the HTTP status code to determine the category of error before parsing the response body.

Parse the Error Body

Read the error field for a human-readable message. Check details for field-level validation errors on 400 responses.

Handle 401 by Re-Authenticating

When you receive a 401, redirect the user to sign in or refresh the session.

Handle 429 with Backoff

When rate limited, wait for the number of seconds in retryAfter (or the X-RateLimit-Reset header) before retrying.

Do Not Parse 500 Messages

Internal server errors are generic by design. Log the error for debugging but show a generic message to users.

How is this guide?

Edit on GitHub

Last updated on

On this page