RESTful API Conventions
Think of a RESTful API like a well-organized library. Just as a library has standard ways to organize and access books, RESTful APIs have standard patterns for organizing and accessing resources.
Core Resource Endpoints
// Example: Book Resource API
// 1. Get all books
GET /books
Response: {
"books": [
{ "id": 1, "title": "The Great Gatsby" },
{ "id": 2, "title": "1984" }
],
"total": 2
}
// 2. Create a new book
POST /books
Request Body: {
"title": "Brave New World",
"author": "Aldous Huxley",
"year": 1932
}
Response: {
"id": 3,
"title": "Brave New World",
"author": "Aldous Huxley",
"year": 1932,
"created_at": "2024-03-15T10:30:00Z"
}
// 3. Get a specific book
GET /books/3
Response: {
"id": 3,
"title": "Brave New World",
"author": "Aldous Huxley",
"year": 1932
}
// 4. Update a book
PUT /books/3
Request Body: {
"title": "Brave New World",
"author": "Aldous Huxley",
"year": 1932,
"edition": "Revised"
}
// 5. Delete a book
DELETE /books/3
Response: {
"message": "Book successfully deleted",
"id": 3
}
Nested Resources
Nested resources represent relationships between different types of data. For example, books can have reviews:
// Get all reviews for a book
GET /books/3/reviews
Response: {
"reviews": [
{
"id": 1,
"rating": 5,
"text": "A masterpiece!",
"user_id": 42
},
{
"id": 2,
"rating": 4,
"text": "Thought-provoking",
"user_id": 57
}
]
}
// Add a new review to a book
POST /books/3/reviews
Request Body: {
"rating": 5,
"text": "Changed my perspective",
"user_id": 101
}
// Update a specific review
PUT /reviews/2
Request Body: {
"rating": 3,
"text": "Updated thoughts after second reading"
}
// Delete a specific review
DELETE /reviews/2
Special Resource Patterns
Some resources might have special patterns that don't follow standard CRUD operations:
// Current weather example - Special identifier
GET /weather/current
Response: {
"temperature": 72,
"conditions": "sunny",
"timestamp": "2024-03-15T15:30:00Z"
}
// Search functionality
GET /books/search?q=brave+new&genre=fiction
Response: {
"results": [
{
"id": 3,
"title": "Brave New World",
"relevance": 0.95
}
],
"total": 1
}
// Batch operations
POST /books/batch
Request Body: {
"operation": "delete",
"ids": [1, 2, 3]
}
Response: {
"success": true,
"deleted": 3,
"failed": 0
}
API Documentation Best Practices
1. Endpoint Documentation
/**
* Create Book
* POST /api/v1/books
*
* Creates a new book in the library system.
*
* Request Body:
* {
* "title": string (required) - Book title
* "author": string (required) - Author's name
* "isbn": string (required) - Valid ISBN-13
* "genre": string[] - Array of genres
* "published_date": string - ISO 8601 date
* }
*
* Response: 201 Created
* {
* "id": number - Unique book identifier
* "title": string - Book title
* "author": string - Author's name
* "isbn": string - ISBN-13
* "genre": string[] - Array of genres
* "published_date": string - ISO 8601 date
* "created_at": string - ISO 8601 timestamp
* }
*
* Errors:
* - 400 Bad Request: Invalid input data
* - 409 Conflict: ISBN already exists
* - 422 Unprocessable Entity: Invalid ISBN format
*/
// Implementation example
async function createBook(bookData) {
const response = await fetch('/api/v1/books', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(bookData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return response.json();
}
2. Query Parameters
/**
* List Books
* GET /api/v1/books
*
* Returns a paginated list of books.
*
* Query Parameters:
* - page (number, optional): Page number, default: 1
* - limit (number, optional): Items per page, default: 20
* - sort (string, optional): Sort field, e.g., "title", "-published_date"
* - genre (string, optional): Filter by genre
* - search (string, optional): Search in title and author
*
* Response: 200 OK
* {
* "books": Book[],
* "total": number,
* "page": number,
* "total_pages": number
* }
*/
// Implementation example
function getQueryString(params) {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined) {
searchParams.append(key, value);
}
}
return searchParams.toString();
}
async function listBooks(params = {}) {
const queryString = getQueryString(params);
const response = await fetch(`/api/v1/books?${queryString}`);
if (!response.ok) {
throw new Error('Failed to fetch books');
}
return response.json();
}
Error Handling Patterns
/**
* Standard Error Response Format
* {
* "error": {
* "code": string,
* "message": string,
* "details": object[],
* "request_id": string
* }
* }
*/
// Error handling middleware example
function errorHandler(err, req, res, next) {
const error = {
code: err.code || 'INTERNAL_ERROR',
message: err.message || 'An unexpected error occurred',
request_id: req.id
};
if (err.details) {
error.details = err.details;
}
const status = err.status || 500;
res.status(status).json({ error });
}
// Validation error example
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{
"field": "isbn",
"message": "Invalid ISBN format"
},
{
"field": "published_date",
"message": "Must be a valid date"
}
],
"request_id": "req_abc123"
}
}
Rate Limiting and Authentication
/**
* Authentication Headers
* Authorization: Bearer your-token-here
*
* Rate Limit Headers
* X-RateLimit-Limit: 100
* X-RateLimit-Remaining: 95
* X-RateLimit-Reset: 1615825200
*/
async function authenticatedRequest(endpoint, options = {}) {
const token = await getAuthToken();
const response = await fetch(endpoint, {
...options,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers
}
});
// Check rate limits
const rateLimit = {
limit: response.headers.get('X-RateLimit-Limit'),
remaining: response.headers.get('X-RateLimit-Remaining'),
reset: response.headers.get('X-RateLimit-Reset')
};
if (rateLimit.remaining === '0') {
const resetDate = new Date(rateLimit.reset * 1000);
throw new Error(`Rate limit exceeded. Resets at ${resetDate}`);
}
return response;
}