Error Codes
Standard error codes and responses used across Y3NKO APIs.
Error Response Format
All API errors follow a consistent format:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message",
"details": {
// Optional additional context
}
}
}HTTP Status Codes
| Status | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Bad Request - Invalid input |
| 401 | Unauthorized - Authentication required |
| 403 | Forbidden - Insufficient permissions |
| 404 | Not Found - Resource doesn’t exist |
| 409 | Conflict - Resource conflict |
| 422 | Unprocessable Entity - Validation failed |
| 429 | Too Many Requests - Rate limited |
| 500 | Internal Server Error |
Error Codes Reference
Authentication Errors (401)
| Code | Message | Description |
|---|---|---|
UNAUTHORIZED | Authentication required | No valid session found |
SESSION_EXPIRED | Your session has expired | Token needs refresh |
INVALID_CREDENTIALS | Invalid email or password | Login failed |
EMAIL_NOT_CONFIRMED | Please verify your email | Email not confirmed |
Authorization Errors (403)
| Code | Message | Description |
|---|---|---|
FORBIDDEN | You don’t have permission | Role insufficient |
NOT_YOUR_RESOURCE | This doesn’t belong to you | Accessing another user’s data |
ROLE_REQUIRED | Admin access required | Admin role needed |
Validation Errors (400/422)
| Code | Message | Description |
|---|---|---|
VALIDATION_ERROR | Invalid input data | Zod validation failed |
INVALID_DATE | Invalid date format | Date parsing failed |
INVALID_PHONE | Invalid phone number | Phone format wrong |
INVALID_EMAIL | Invalid email address | Email format wrong |
MISSING_REQUIRED | Required field missing | Required field not provided |
Validation Error Details:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": {
"issues": [
{
"path": ["guestPhone"],
"message": "Phone must start with +233",
"code": "invalid_string"
},
{
"path": ["checkOut"],
"message": "Check-out must be after check-in",
"code": "custom"
}
]
}
}
}Resource Errors (404/409)
| Code | Message | Description |
|---|---|---|
NOT_FOUND | Resource not found | Requested item doesn’t exist |
LISTING_NOT_FOUND | Listing not found | Listing doesn’t exist or unpublished |
BOOKING_NOT_FOUND | Booking not found | Booking reference invalid |
USER_NOT_FOUND | User not found | User doesn’t exist |
DATES_UNAVAILABLE | Selected dates unavailable | Dates already booked |
ALREADY_EXISTS | Resource already exists | Duplicate creation attempt |
ALREADY_CANCELLED | Booking already cancelled | Can’t cancel twice |
ALREADY_PAID | Payment already completed | Duplicate payment attempt |
Payment Errors
| Code | Message | Description |
|---|---|---|
PAYMENT_FAILED | Payment failed | Paystack returned error |
PAYMENT_DECLINED | Payment was declined | Card/MoMo declined |
INSUFFICIENT_FUNDS | Insufficient funds | Not enough balance |
INVALID_CARD | Invalid card details | Card validation failed |
REFUND_FAILED | Refund processing failed | Refund couldn’t be processed |
Rate Limiting (429)
| Code | Message | Description |
|---|---|---|
RATE_LIMITED | Too many requests | Rate limit exceeded |
Rate Limit Headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1709912345
Retry-After: 60Server Errors (500)
| Code | Message | Description |
|---|---|---|
INTERNAL_ERROR | Something went wrong | Unexpected server error |
DATABASE_ERROR | Database error | DB query failed |
EXTERNAL_SERVICE_ERROR | External service error | Paystack/Cloudinary/Resend failed |
Handling Errors
Client-Side
async function createBooking(data: BookingInput) {
try {
const response = await fetch('/api/bookings', {
method: 'POST',
body: JSON.stringify(data)
})
if (!response.ok) {
const error = await response.json()
switch (error.error.code) {
case 'DATES_UNAVAILABLE':
toast.error('These dates are no longer available')
break
case 'VALIDATION_ERROR':
// Show field-specific errors
error.error.details.issues.forEach(issue => {
setFieldError(issue.path[0], issue.message)
})
break
case 'UNAUTHORIZED':
router.push('/login')
break
default:
toast.error(error.error.message)
}
return null
}
return response.json()
} catch (e) {
toast.error('Network error. Please try again.')
return null
}
}Server-Side
// lib/errors.ts
export class AppError extends Error {
constructor(
public code: string,
public message: string,
public status: number = 400,
public details?: any
) {
super(message)
}
}
export function handleError(error: unknown) {
if (error instanceof AppError) {
return NextResponse.json(
{
error: {
code: error.code,
message: error.message,
details: error.details
}
},
{ status: error.status }
)
}
console.error('Unexpected error:', error)
return NextResponse.json(
{
error: {
code: 'INTERNAL_ERROR',
message: 'Something went wrong'
}
},
{ status: 500 }
)
}Usage in API Routes
// app/api/bookings/route.ts
import { AppError, handleError } from '@/lib/errors'
export async function POST(request: Request) {
try {
const data = await request.json()
// Validation
const result = bookingSchema.safeParse(data)
if (!result.success) {
throw new AppError(
'VALIDATION_ERROR',
'Invalid input data',
400,
{ issues: result.error.issues }
)
}
// Check availability
const available = await checkAvailability(data.listingId, data.checkIn, data.checkOut)
if (!available) {
throw new AppError(
'DATES_UNAVAILABLE',
'Selected dates are no longer available',
409
)
}
// Create booking...
} catch (error) {
return handleError(error)
}
}Logging
Errors are logged server-side with context:
console.error({
code: error.code,
message: error.message,
userId: session?.user?.id,
path: request.url,
timestamp: new Date().toISOString()
})In production, consider using a proper logging service (Sentry, LogRocket, etc.).