GET Design Guidelines — Safe, Idempotent, and Cacheable Resource Retrieval
In this tutorial, you will learn about GET Design Guidelines. We cover key concepts, practical examples, and best practices to help you master this topic.
GET is the most common HTTP method in REST APIs, used for safe, idempotent, and cacheable resource retrieval. Proper GET design includes query parameters for filtering, conditional headers for efficiency, and pagination for large collections.
What You'll Learn
- How to design safe and cacheable GET endpoints
- How to handle filtering, sorting, and pagination with GET
- How to implement conditional GET requests
Why It Matters
GET requests account for 80%+ of API traffic. Optimizing GET endpoints with Caching, proper status codes, and efficient query parameters reduces server load by 5-10x and improves client experience through faster responses.
Real-World Use
DodaTech's public API handles 50,000 GET requests per second. ETag-based conditional requests mean 60% of responses are 304 Not Modified. Proper caching headers allow CDNs to serve 90% of GET requests from edge cache with sub-10ms response times.
flowchart LR
A["GET Request"] --> B{"Has\nAPI Key?"}
B -->|No| C["401\nUnauthorized"]
B -->|Yes| D{"Has valid\nETag?"}
D -->|Yes| E["304 Not\nModified"]
D -->|No| F["Process\nRequest"]
F --> G{"Has query\nfilters?"}
G -->|Yes| H["Filter\nResults"]
G -->|No| I["Return\nAll"]
H --> J["200 OK +\nETag"]
I --> J
style A fill:#bbf7d0,stroke:#16a34a
style E fill:#fef3c7,stroke:#d97706
style J fill:#dbeafe,stroke:#2563eb
Safe GET Implementation
GET must never change server state:
from flask import Flask, jsonify, request, make_response
app = Flask(__name__)
@app.route('/api/products', methods=['GET'])
def list_products():
# No side effects - only read data
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
products, total = database.get_products_paginated(page, per_page)
response_data = {
"data": [p.to_dict() for p in products],
"page": page,
"per_page": per_page,
"total": total
}
response = make_response(jsonify(response_data))
response.headers['Cache-Control'] = 'public, max-age=60'
return response
Conditional GET with ETag
@app.route('/api/products/<int:product_id>', methods=['GET'])
def get_product(product_id):
product = database.get_product(product_id)
if not product:
return jsonify({"error": "not_found"}), 404
current_etag = compute_etag(product)
# Client sends If-None-Match with previously received ETag
if_none_match = request.headers.get('If-None-Match')
if if_none_match == current_etag:
return '', 304
response = make_response(jsonify(product.to_dict()))
response.headers['ETag'] = current_etag
response.headers['Cache-Control'] = 'private, max-age=60'
response.headers['Last-Modified'] = product.updated_at.strftime(
'%a, %d %b %Y %H:%M:%S GMT'
)
return response
Filtering, Sorting, Pagination
@app.route('/api/products', methods=['GET'])
def list_products():
# Filtering via query parameters
category = request.args.get('category')
min_price = request.args.get('min_price', type=float)
max_price = request.args.get('max_price', type=float)
in_stock = request.args.get('in_stock', type=bool)
# Sorting
sort_by = request.args.get('sort_by', 'created_at')
sort_order = request.args.get('sort_order', 'desc')
# Pagination
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
# Validate inputs
allowed_sort_fields = ['name', 'price', 'created_at', 'updated_at']
if sort_by not in allowed_sort_fields:
return jsonify({
"error": "invalid_sort",
"message": f"Sort by must be one of: {allowed_sort_fields}"
}), 400
if per_page > 100:
return jsonify({
"error": "per_page_limit",
"message": "Maximum per_page is 100"
}), 400
products, total = database.query_products(
category=category,
min_price=min_price,
max_price=max_price,
in_stock=in_stock,
sort_by=sort_by,
sort_order=sort_order,
page=page,
per_page=per_page
)
return jsonify({
"data": [p.to_dict() for p in products],
"pagination": {
"page": page,
"per_page": per_page,
"total": total,
"total_pages": (total + per_page - 1) // per_page
},
"_links": {
"self": {"href": f"/api/products?page={page}&per_page={per_page}"},
"next": {"href": f"/api/products?page={page+1}&per_page={per_page}"}
if page * per_page < total else None,
"prev": {"href": f"/api/products?page={page-1}&per_page={per_page}"}
if page > 1 else None
}
})
Common Mistakes
1. Using POST for Complex GET Requests
When GET requests need complex query parameters, some developers fall back to POST with a body. Instead, encode complex queries as query string parameters or use the Accept header.
2. Not Returning Useful 404 Details
A 404 response should indicate which resource was not found. Return {"error": "not_found", "resource": "product", "id": 123} for better debugging.
3. Ignoring Range Requests
For large collections, support Range headers so clients can request byte ranges or offset-based subsets.
4. Not Limiting Per-Page
Always cap pagination limits. Returning 1 million records in a single response is never appropriate.
5. Failing to Handle If-Modified-Since
Support both ETag and Last-Modified based conditional requests for maximum client compatibility.
Practice Questions
- What makes GET a safe method?
- How does conditional GET reduce server load?
- What headers should a GET response include for caching?
- How do you handle query parameter validation?
- What is the maximum recommended per_page value?
Answers
- GET should never modify server state. 2. Returns 304 Not Modified when the client's cached copy is current, avoiding body transfer. 3. Cache-Control, ETag, Last-Modified. 4. Validate against an allowed list and return 400 with error details for invalid parameters. 5. 100 items per page.
Challenge
Build a product catalog API with: filtering by multiple criteria (category, price range, in-stock), sorting by any allowed field, pagination with correct total counts and links, conditional GET with ETags and If-Modified-Since, and comprehensive input validation.
FAQ
Mini Project
Build a search API with comprehensive GET guidelines: supports field-specific filtering, multiple sort options, cursor-based pagination, ETag-based conditional requests, range requests for large datasets, and input validation with descriptive error messages.
What's Next
- Learn about POST design guidelines for resource creation
- Explore PUT vs PATCH for complete vs partial updates
- Continue to idempotency guarantees for PUT and DELETE
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro