What is REST?
REST (Representational State Transfer) is a set of conventions for building web APIs. Instead of inventing a custom protocol, you expose your data as "resources" (like users or posts) and let clients act on them with normal HTTP requests. Because it reuses HTTP, REST is simple to build, easy to scale, and works with any language that can make a web request.
Core Principles
1. Stateless Communication
- Each request contains all necessary information
- No client context stored on server
- Improves scalability and reliability
- Easier to cache and debug
Stateless means the server remembers nothing between requests: every call must carry everything needed to handle it (like a login token). Any server in a pool can answer any request, which is why this scales well.
2. Standard HTTP Methods
Each HTTP method maps to a basic data operation, summarized as CRUD (Create, Read, Update, Delete):
# CRUD Operations
GET /api/users # Read users
POST /api/users # Create user
PUT /api/users/1 # Update user
DELETE /api/users/1 # Delete user
# Additional Methods
PATCH /api/users/1 # Partial update
HEAD /api/users # Get headers only
Implementation Examples
Here is a minimal API written with Flask, a small Python web framework. Each function below is an "endpoint" — a URL the client can call.
1. Basic REST API in Python
from flask import Flask, jsonify, request
app = Flask(__name__)
# GET endpoint
@app.route('/api/users', methods=['GET'])
def get_users():
return jsonify({
'users': users,
'total': len(users)
})
# POST endpoint
@app.route('/api/users', methods=['POST'])
def create_user():
user = request.json
users.append(user)
return jsonify(user), 201
# PUT endpoint
@app.route('/api/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
user = next((u for u in users if u['id'] == user_id), None)
if user:
user.update(request.json)
return jsonify(user)
return jsonify({'error': 'User not found'}), 404
2. Response Formats
Responses are usually JSON. A good API keeps a consistent shape: data on success, a clear error object on failure.
// Success Response
{
"data": {
"id": 1,
"name": "John Doe",
"email": "[email protected]"
},
"meta": {
"timestamp": "2025-01-20T10:00:00Z"
}
}
// Error Response
{
"error": {
"code": "NOT_FOUND",
"message": "User not found",
"details": "No user exists with ID 123"
}
}
Best Practices
1. URL Structure
Treat each URL as a path to a resource. Use plural nouns, nest related resources, and put options in query parameters (the part after ?):
# Resource Hierarchy
/api/v1/users # User collection
/api/v1/users/{id} # Specific user
/api/v1/users/{id}/posts # User's posts
/api/v1/users/{id}/posts/{id} # Specific post
# Query Parameters
/api/v1/users?role=admin # Filtering
/api/v1/users?sort=name # Sorting
/api/v1/users?page=2&limit=10 # Pagination
2. Authentication
Authentication proves who is calling. A common approach is JWT (JSON Web Token) — the server hands back a signed token at login, and the client sends it on every later request to prove access:
# JWT Authentication Example
from flask_jwt_extended import jwt_required, create_access_token
@app.route('/api/login', methods=['POST'])
def login():
username = request.json.get('username')
password = request.json.get('password')
if authenticate_user(username, password):
access_token = create_access_token(identity=username)
return jsonify({'token': access_token})
return jsonify({'error': 'Invalid credentials'}), 401
@app.route('/api/protected', methods=['GET'])
@jwt_required()
def protected_route():
return jsonify({'message': 'Access granted'})
3. Rate Limiting
Rate limiting caps how many requests a caller can make in a given window, protecting the server from overload or abuse:
from flask_limiter import Limiter
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
@app.route('/api/users')
@limiter.limit("1 per second")
def get_users():
return jsonify(users)
Common Features
1. Pagination
When a collection is large, return it in pages instead of all at once. The client asks for a page number and size; the server returns that slice plus totals:
@app.route('/api/users')
def get_users():
page = int(request.args.get('page', 1))
limit = int(request.args.get('limit', 10))
start = (page - 1) * limit
end = start + limit
return jsonify({
'data': users[start:end],
'meta': {
'total': len(users),
'page': page,
'limit': limit,
'pages': ceil(len(users) / limit)
}
})
2. Filtering and Sorting
Let clients narrow and order results through query parameters, so they fetch only what they need:
@app.route('/api/users')
def get_users():
# Filtering
role = request.args.get('role')
if role:
filtered_users = [u for u in users if u['role'] == role]
# Sorting
sort_by = request.args.get('sort')
if sort_by:
filtered_users.sort(key=lambda x: x[sort_by])
return jsonify(filtered_users)
Security Considerations
1. Input Validation
Never trust incoming data. Validate it against a schema (a definition of allowed fields and types) before using it, and reject anything malformed:
from marshmallow import Schema, fields
class UserSchema(Schema):
name = fields.Str(required=True)
email = fields.Email(required=True)
age = fields.Int(validate=lambda n: n >= 0)
@app.route('/api/users', methods=['POST'])
def create_user():
schema = UserSchema()
try:
data = schema.load(request.json)
# Process validated data
return jsonify(data), 201
except ValidationError as err:
return jsonify(err.messages), 400
2. CORS Handling
CORS (Cross-Origin Resource Sharing) is the browser rule that controls which websites may call your API from their own pages. Configure it to allow only the origins, methods, and headers you trust:
from flask_cors import CORS
# Configure CORS
CORS(app, resources={
r"/api/*": {
"origins": ["https://allowed-domain.com"],
"methods": ["GET", "POST", "PUT", "DELETE"],
"allow_headers": ["Content-Type", "Authorization"]
}
})
Remember: A well-designed REST API should be intuitive, consistent, and secure while following established conventions and best practices.
