Python RESTful APIs
REST (Representational State Transfer) APIs have become the standard method for creating web services. In this guide, you'll learn how to both consume and build RESTful APIs using Python, a language known for its simplicity and powerful web development capabilities.
What is a RESTful API?
A RESTful API is an architectural style for designing networked applications. It uses HTTP requests to access and manipulate data, following specific principles:
- Resource-based: Everything is a resource identified by a URL
- Stateless: Each request contains all information needed to process it
- Standard HTTP methods: Uses GET, POST, PUT, DELETE, etc.
- Standard data formats: Typically uses JSON or XML
Consuming RESTful APIs with Python
Before building your own API, let's learn how to consume existing APIs with Python's popular requests
library.
Setting Up
First, install the requests library:
pip install requests
Making Basic Requests
Let's start with a simple GET request to fetch data from a public API:
import requests
# Make a GET request to a public API
response = requests.get('https://jsonplaceholder.typicode.com/posts/1')
# Check if the request was successful
if response.status_code == 200:
# Parse the JSON response
data = response.json()
print(f"Post title: {data['title']}")
print(f"Post body: {data['body']}")
else:
print(f"Request failed with status code: {response.status_code}")
Output:
Post title: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Post body: quia et suscipit suscipit recusandae consequuntur expedita et cum reprehenderit molestiae ut ut quas totam nostrum rerum est autem sunt rem eveniet architecto
Working with Different HTTP Methods
REST APIs use different HTTP methods for different operations:
import requests
import json
# Base URL for our API
base_url = 'https://jsonplaceholder.typicode.com'
# GET: Retrieve data
get_response = requests.get(f'{base_url}/posts/1')
print(f"GET status: {get_response.status_code}")
# POST: Create new data
new_post = {
'title': 'New Post',
'body': 'This is the content of my new post',
'userId': 1
}
post_response = requests.post(
f'{base_url}/posts',
data=json.dumps(new_post),
headers={'Content-type': 'application/json; charset=UTF-8'}
)
print(f"POST status: {post_response.status_code}")
print(f"Created post ID: {post_response.json().get('id')}")
# PUT: Update existing data
updated_post = {
'id': 1,
'title': 'Updated Title',
'body': 'Updated content',
'userId': 1
}
put_response = requests.put(
f'{base_url}/posts/1',
data=json.dumps(updated_post),
headers={'Content-type': 'application/json; charset=UTF-8'}
)
print(f"PUT status: {put_response.status_code}")
# DELETE: Remove data
delete_response = requests.delete(f'{base_url}/posts/1')
print(f"DELETE status: {delete_response.status_code}")
Output:
GET status: 200
POST status: 201
Created post ID: 101
PUT status: 200
DELETE status: 200
Handling Authentication
Many APIs require authentication. Here's how to use different authentication methods:
import requests
# Basic authentication
response = requests.get(
'https://api.example.com/data',
auth=('username', 'password')
)
# API key authentication (in header)
headers = {
'Authorization': 'Bearer YOUR_API_KEY'
}
response = requests.get(
'https://api.example.com/data',
headers=headers
)
# API key as a parameter
params = {
'api_key': 'YOUR_API_KEY'
}
response = requests.get(
'https://api.example.com/data',
params=params
)
Building RESTful APIs with Flask
Now let's learn how to build our own RESTful API using Flask, a lightweight Python web framework.
Setting Up Flask
First, install Flask and its extension for RESTful APIs:
pip install flask flask-restful
Creating a Simple REST API
Let's create a simple API for managing a collection of books:
from flask import Flask, request, jsonify
app = Flask(__name__)
# In-memory data store
books = [
{"id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald"},
{"id": 2, "title": "To Kill a Mockingbird", "author": "Harper Lee"}
]
# GET all books
@app.route('/api/books', methods=['GET'])
def get_books():
return jsonify(books)
# GET a specific book
@app.route('/api/books/<int:book_id>', methods=['GET'])
def get_book(book_id):
book = next((b for b in books if b['id'] == book_id), None)
if book:
return jsonify(book)
return jsonify({"error": "Book not found"}), 404
# POST a new book
@app.route('/api/books', methods=['POST'])
def add_book():
if not request.json or not 'title' in request.json:
return jsonify({"error": "Title is required"}), 400
# Generate a new ID
new_id = max(book['id'] for book in books) + 1 if books else 1
book = {
'id': new_id,
'title': request.json['title'],
'author': request.json.get('author', '')
}
books.append(book)
return jsonify(book), 201
# PUT (update) a book
@app.route('/api/books/<int:book_id>', methods=['PUT'])
def update_book(book_id):
book = next((b for b in books if b['id'] == book_id), None)
if not book:
return jsonify({"error": "Book not found"}), 404
if not request.json:
return jsonify({"error": "No data provided"}), 400
book['title'] = request.json.get('title', book['title'])
book['author'] = request.json.get('author', book['author'])
return jsonify(book)
# DELETE a book
@app.route('/api/books/<int:book_id>', methods=['DELETE'])
def delete_book(book_id):
global books
initial_length = len(books)
books = [b for b in books if b['id'] != book_id]
if len(books) < initial_length:
return jsonify({"message": "Book deleted"}), 200
return jsonify({"error": "Book not found"}), 404
if __name__ == '__main__':
app.run(debug=True)
To run this API, save the code to a file like app.py
and execute it:
python app.py
Your API will be available at http://127.0.0.1:5000/
.
Testing the API
You can use tools like Postman, or Python's requests
library to test your API:
import requests
base_url = 'http://127.0.0.1:5000/api/books'
# Get all books
response = requests.get(base_url)
print("All books:", response.json())
# Add a new book
new_book = {"title": "1984", "author": "George Orwell"}
response = requests.post(base_url, json=new_book)
print("Added book:", response.json())
# Get a specific book
book_id = response.json()['id'] # Use the ID from the book we just added
response = requests.get(f"{base_url}/{book_id}")
print(f"Book {book_id}:", response.json())
# Update a book
updated_info = {"title": "1984", "author": "George Orwell (Updated)"}
response = requests.put(f"{base_url}/{book_id}", json=updated_info)
print("Updated book:", response.json())
# Delete a book
response = requests.delete(f"{base_url}/{book_id}")
print("Delete response:", response.json())
Using Flask-RESTful for More Structured APIs
Flask-RESTful is an extension that adds support for quickly building REST APIs. It's a common choice for more complex APIs:
from flask import Flask
from flask_restful import Resource, Api, reqparse
app = Flask(__name__)
api = Api(app)
# In-memory data store
books = [
{"id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald"},
{"id": 2, "title": "To Kill a Mockingbird", "author": "Harper Lee"}
]
# Create parser for book data
parser = reqparse.RequestParser()
parser.add_argument('title', type=str, required=True, help='Title cannot be blank')
parser.add_argument('author', type=str, required=False)
# Resource for collection of books
class BookList(Resource):
def get(self):
return books
def post(self):
args = parser.parse_args()
new_id = max(book['id'] for book in books) + 1 if books else 1
book = {
'id': new_id,
'title': args['title'],
'author': args['author'] or ''
}
books.append(book)
return book, 201
# Resource for a single book
class Book(Resource):
def get(self, book_id):
book = next((b for b in books if b['id'] == book_id), None)
if book:
return book
return {"error": "Book not found"}, 404
def put(self, book_id):
book = next((b for b in books if b['id'] == book_id), None)
if not book:
return {"error": "Book not found"}, 404
args = parser.parse_args()
book['title'] = args['title']
book['author'] = args['author'] or book['author']
return book
def delete(self, book_id):
global books
initial_length = len(books)
books = [b for b in books if b['id'] != book_id]
if len(books) < initial_length:
return {"message": "Book deleted"}, 200
return {"error": "Book not found"}, 404
# Register resources
api.add_resource(BookList, '/api/books')
api.add_resource(Book, '/api/books/<int:book_id>')
if __name__ == '__main__':
app.run(debug=True)
Best Practices for RESTful APIs
When designing your APIs, keep these best practices in mind:
-
Use proper HTTP methods:
- GET for retrieving data
- POST for creating data
- PUT/PATCH for updating data
- DELETE for removing data
-
Return appropriate status codes:
- 200 OK: Request succeeded
- 201 Created: Resource created successfully
- 400 Bad Request: Invalid request format
- 404 Not Found: Resource not found
- 500 Server Error: Something went wrong on the server
-
Version your API: Add a version number in the URL path (e.g.,
/api/v1/books
) -
Implement proper error handling: Return meaningful error messages and appropriate status codes
-
Use pagination for large datasets:
@app.route('/api/books', methods=['GET'])
def get_books():
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 10))
start = (page - 1) * per_page
end = start + per_page
book_slice = books[start:end]
return jsonify({
'books': book_slice,
'total': len(books),
'page': page,
'per_page': per_page,
'pages': (len(books) + per_page - 1) // per_page
})
- Document your API: Use tools like Swagger/OpenAPI to document your endpoints
Real-World Example: Building a Weather API Client
Let's create a practical example by building a client for the OpenWeatherMap API:
import requests
import os
from datetime import datetime
class WeatherClient:
def __init__(self, api_key=None):
self.api_key = api_key or os.environ.get('OPENWEATHER_API_KEY')
if not self.api_key:
raise ValueError("API key is required")
self.base_url = "https://api.openweathermap.org/data/2.5"
def get_current_weather(self, city, units="metric"):
"""Get current weather for a city"""
endpoint = f"{self.base_url}/weather"
params = {
"q": city,
"appid": self.api_key,
"units": units
}
response = requests.get(endpoint, params=params)
if response.status_code == 200:
data = response.json()
return {
"city": data["name"],
"country": data["sys"]["country"],
"temperature": data["main"]["temp"],
"feels_like": data["main"]["feels_like"],
"description": data["weather"][0]["description"],
"humidity": data["main"]["humidity"],
"wind_speed": data["wind"]["speed"],
"timestamp": datetime.fromtimestamp(data["dt"]).strftime('%Y-%m-%d %H:%M:%S')
}
elif response.status_code == 404:
return {"error": "City not found"}
else:
return {"error": f"API request failed with status code {response.status_code}"}
def get_forecast(self, city, days=5, units="metric"):
"""Get weather forecast for a city"""
endpoint = f"{self.base_url}/forecast"
params = {
"q": city,
"appid": self.api_key,
"units": units,
"cnt": days * 8 # 8 measurements per day (every 3 hours)
}
response = requests.get(endpoint, params=params)
if response.status_code == 200:
data = response.json()
forecasts = []
for item in data["list"]:
forecasts.append({
"datetime": item["dt_txt"],
"temperature": item["main"]["temp"],
"description": item["weather"][0]["description"],
"humidity": item["main"]["humidity"],
"wind_speed": item["wind"]["speed"]
})
return {
"city": data["city"]["name"],
"country": data["city"]["country"],
"forecasts": forecasts
}
elif response.status_code == 404:
return {"error": "City not found"}
else:
return {"error": f"API request failed with status code {response.status_code}"}
# Example usage
if __name__ == "__main__":
# Get API key from OpenWeatherMap website
api_key = "your_api_key_here"
weather = WeatherClient(api_key)
# Get current weather
current = weather.get_current_weather("London")
print(f"Current weather in {current['city']}:")
print(f"Temperature: {current['temperature']}°C")
print(f"Description: {current['description']}")
print(f"Humidity: {current['humidity']}%")
# Get forecast
forecast = weather.get_forecast("London", days=2)
print("\nForecast:")
for f in forecast["forecasts"][:3]: # Show first 3 forecast periods
print(f"{f['datetime']}: {f['temperature']}°C, {f['description']}")
To use this client, you'll need to sign up for a free API key at OpenWeatherMap.
Summary
In this guide, you learned how to:
- Understand the principles of RESTful APIs
- Consume REST APIs using Python's
requests
library - Build your own REST API using Flask and Flask-RESTful
- Follow best practices for API design
- Create a real-world example with the OpenWeatherMap API
RESTful APIs are a fundamental component of modern web development, and Python provides excellent tools for both consuming and building them. As you continue your journey, you'll find these skills invaluable for creating scalable and maintainable web applications.
Additional Resources
- Flask Documentation
- Flask-RESTful Documentation
- Requests Library Documentation
- RESTful API Design Best Practices
- Postman - API Testing Tool
Exercises
- Extend the book API to include search functionality (search by title or author)
- Add user authentication to the Flask API using JWT tokens
- Create a client for a public API of your choice (GitHub, Twitter, Spotify, etc.)
- Modify the Weather API client to include error handling and automatic retries
- Implement pagination in the book API for large collections
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)