Why Your REST API Should Return 202 Instead of 200
A practical guide to implementing the 202 Accepted pattern in REST APIs. Learn how to handle long-running operations, track progress, and improve system architecture.
Most APIs blindly return 200 OK for successful operations. Here's why using 202 Accepted can lead to better architecture and user experience.
The Problem with 200 OK
// ❌ BAD: Synchronous processing with 200
app.post('/api/orders', async (req, res) => {
try {
// Blocking operations
await validateOrder(req.body);
await processPayment(req.body.payment);
await createOrder(req.body);
await sendConfirmationEmail(req.body.email);
res.status(200).json({ message: 'Order created' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
This approach has several problems:
- Long request times
- Timeout risks
- Poor user experience
- Resource wastage
The 202 Accepted Pattern
1. Basic Implementation
// ✅ BETTER: Asynchronous processing with 202
app.post('/api/orders', async (req, res) => {
try {
// Generate job ID
const jobId = uuid();
// Queue the work
await queue.add('create-order', {
jobId,
order: req.body
});
// Return immediately
res.status(202).json({
jobId,
status: 'processing',
statusUrl: `/api/orders/status/${jobId}`
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
2. Status Endpoint
app.get('/api/orders/status/:jobId', async (req, res) => {
const status = await getJobStatus(req.params.jobId);
res.json({
jobId: req.params.jobId,
status: status.state,
result: status.result,
estimatedCompletion: status.eta
});
});
Real World Implementation
1. Queue Processing
// queue-processor.ts
interface Job {
jobId: string;
order: OrderData;
}
const orderProcessor = async (job: Job) => {
try {
// Update status
await updateJobStatus(job.jobId, 'validating');
await validateOrder(job.order);
await updateJobStatus(job.jobId, 'processing-payment');
await processPayment(job.order.payment);
await updateJobStatus(job.jobId, 'creating-order');
const order = await createOrder(job.order);
await updateJobStatus(job.jobId, 'sending-email');
await sendConfirmationEmail(job.order.email);
// Complete job
await updateJobStatus(job.jobId, 'completed', { orderId: order.id });
} catch (error) {
await updateJobStatus(job.jobId, 'failed', { error: error.message });
}
};
2. Status Tracking
// status-tracker.ts
interface JobStatus {
state: 'queued' | 'processing' | 'completed' | 'failed';
progress?: number;
result?: any;
error?: string;
updatedAt: Date;
}
class JobTracker {
async updateStatus(jobId: string, status: Partial<JobStatus>) {
await redis.hset(`job:${jobId}`, {
...status,
updatedAt: new Date().toISOString()
});
}
async getStatus(jobId: string): Promise<JobStatus> {
return await redis.hgetall(`job:${jobId}`);
}
}
Client Implementation
1. Polling Pattern
// client-side.ts
class OrderService {
async createOrder(orderData: OrderData) {
// Initial submission
const { jobId, statusUrl } = await this.api.post('/orders', orderData);
// Poll for completion
return this.pollJobStatus(statusUrl);
}
private async pollJobStatus(statusUrl: string) {
const maxAttempts = 30;
const interval = 2000;
for (let i = 0; i < maxAttempts; i++) {
const status = await this.api.get(statusUrl);
switch (status.state) {
case 'completed':
return status.result;
case 'failed':
throw new Error(status.error);
default:
await sleep(interval);
}
}
throw new Error('Operation timed out');
}
}
2. WebSocket Updates
// websocket-client.ts
class OrderStatusSocket {
constructor(private jobId: string) {
this.socket = io('/order-status');
this.socket.emit('subscribe', jobId);
}
onStatusUpdate(callback: (status: JobStatus) => void) {
this.socket.on(`status:${this.jobId}`, callback);
}
onComplete(callback: (result: any) => void) {
this.socket.on(`complete:${this.jobId}`, callback);
}
onError(callback: (error: Error) => void) {
this.socket.on(`error:${this.jobId}`, callback);
}
}
Progress Updates
1. Detailed Status Response
{
"jobId": "abc-123",
"status": "processing",
"stage": "payment-processing",
"progress": 45,
"steps": [
{ "name": "validation", "status": "completed" },
{ "name": "payment", "status": "in-progress" },
{ "name": "order-creation", "status": "pending" },
{ "name": "email", "status": "pending" }
],
"estimatedCompletion": "2024-01-20T15:30:00Z"
}
2. Progress Tracking
class ProgressTracker {
private steps = [
'validation',
'payment',
'order-creation',
'email'
];
calculateProgress(currentStep: string): number {
const stepIndex = this.steps.indexOf(currentStep);
return Math.round((stepIndex / this.steps.length) * 100);
}
async updateProgress(jobId: string, step: string) {
const progress = this.calculateProgress(step);
await jobTracker.updateStatus(jobId, {
stage: step,
progress,
updatedAt: new Date()
});
}
}
Benefits
- Better User Experience
- Immediate response
- Progress tracking
- No timeouts
- System Benefits
- Resource optimization
- Better error handling
- Scalability
- Retry capability
- Architectural Benefits
- Decoupled operations
- Better monitoring
- Easier maintenance
Best Practices
- Always provide status URL
- Include estimated completion time
- Implement proper error handling
- Use WebSockets for real-time updates
- Maintain job history
- Implement proper cleanup
- Monitor job queues
Conclusion
Using 202 Accepted:
- Improves user experience
- Enables better architecture
- Provides more control
- Scales better
- Handles errors gracefully
Remember: Not every operation needs to be synchronous.