What is a REST API?
REST (Representational State Transfer) is an architectural style for building web services. A REST API allows different applications to communicate over HTTP using standard methods (GET, POST, PUT, DELETE) and exchange data in formats like JSON.
Think of an API as a waiter in a restaurant: you (the client) tell the waiter (API) what you want from the menu (available endpoints), and the waiter brings it from the kitchen (server/database). You don't need to know how the food is prepared - you just need to know how to order!
Why REST APIs?
- Separation of Concerns: Frontend and backend can be developed independently
- Multiple Clients: Same API serves web, mobile, IoT, third-party apps
- Scalability: Stateless design allows horizontal scaling
- Technology Independence: React frontend can talk to Python backend
- Reusability: One API for many different interfaces
- Standard Protocol: HTTP is universal and well-understood
- Microservices: Break monoliths into independent services
REST API Principles
1. Resources and URLs
- Everything is a resource (user, post, product)
- Each resource has a unique URL
GET /api/users → List all users
GET /api/users/1 → Get user with ID 1
POST /api/users → Create new user
PUT /api/users/1 → Update user 1
DELETE /api/users/1 → Delete user 1
2. HTTP Methods (Verbs)
GET → Retrieve data (safe, idempotent)
POST → Create new resource
PUT → Update entire resource (idempotent)
PATCH → Update partial resource
DELETE → Remove resource (idempotent)
3. Status Codes
200 OK → Success
201 Created → Resource created
204 No Content → Success, no response body
400 Bad Request → Invalid input
401 Unauthorized → Authentication required
403 Forbidden → Authenticated but no permission
404 Not Found → Resource doesn't exist
500 Server Error → Something broke on server
4. Stateless
- Each request contains all needed information
- Server doesn't store client state
- Use tokens (JWT) for authentication
5. JSON Format (standard)
{
"id": 1,
"username": "alice",
"email": "alice@example.com"
}
Django REST Framework (DRF)
DRF is the most popular toolkit for building Web APIs with Django. It's powerful, flexible, and production-ready.
# Install Django REST Framework
pip install djangorestframework
# settings.py
INSTALLED_APPS = [
'rest_framework',
]
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
# models.py
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
isbn = models.CharField(max_length=13, unique=True)
published_date = models.DateField()
price = models.DecimalField(max_digits=6, decimal_places=2)
def __str__(self):
return self.title
# serializers.py (convert models to/from JSON)
from rest_framework import serializers
from .models import Book
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ['id', 'title', 'author', 'isbn', 'published_date', 'price']
# Custom validation
def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError("Price must be positive")
return value
# views.py (using ViewSets - most powerful)
from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
# Custom action
@action(detail=False, methods=['get'])
def recent(self, request):
recent_books = Book.objects.order_by('-published_date')[:5]
serializer = self.get_serializer(recent_books, many=True)
return Response(serializer.data)
# urls.py
from rest_framework.routers import DefaultRouter
from .views import BookViewSet
router = DefaultRouter()
router.register(r'books', BookViewSet)
urlpatterns = [
path('api/', include(router.urls)),
]
# This creates these endpoints automatically:
# GET /api/books/ → List books
# POST /api/books/ → Create book
# GET /api/books/1/ → Get book 1
# PUT /api/books/1/ → Update book 1
# PATCH /api/books/1/ → Partial update
# DELETE /api/books/1/ → Delete book 1
# GET /api/books/recent/ → Custom action
DRF: Authentication and Permissions
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
# Generate token for users
from rest_framework.authtoken.models import Token
token = Token.objects.create(user=user)
print(token.key)
# Client sends token in header:
# Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
# views.py - Custom permissions
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# Read permissions for everyone
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions only for owner
return obj.owner == request.user
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = [IsOwnerOrReadOnly]
# JWT Authentication (more modern)
pip install djangorestframework-simplejwt
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
# urls.py
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path('api/token/', TokenObtainPairView.as_view()),
path('api/token/refresh/', TokenRefreshView.as_view()),
]
# Client workflow:
# 1. POST /api/token/ with username/password
# Returns: access token + refresh token
# 2. Use access token in requests:
# Authorization: Bearer
# 3. When access expires, POST to /api/token/refresh/
# with refresh token to get new access token
Flask-RESTful
Flask-RESTful is an extension for Flask that adds support for building REST APIs quickly.
# Install
pip install flask-restful flask-sqlalchemy
# app.py
from flask import Flask
from flask_restful import Resource, Api, reqparse, fields, marshal_with
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///books.db'
db = SQLAlchemy(app)
api = Api(app)
# Model
class Book(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
author = db.Column(db.String(100), nullable=False)
price = db.Column(db.Float, nullable=False)
def __repr__(self):
return f'<Book {self.title}>'
# Serialization format
book_fields = {
'id': fields.Integer,
'title': fields.String,
'author': fields.String,
'price': fields.Float
}
# Request parser (validation)
book_parser = reqparse.RequestParser()
book_parser.add_argument('title', type=str, required=True, help='Title is required')
book_parser.add_argument('author', type=str, required=True)
book_parser.add_argument('price', type=float, required=True)
# Resources (endpoints)
class BookList(Resource):
@marshal_with(book_fields)
def get(self):
return Book.query.all()
@marshal_with(book_fields)
def post(self):
args = book_parser.parse_args()
book = Book(
title=args['title'],
author=args['author'],
price=args['price']
)
db.session.add(book)
db.session.commit()
return book, 201
class BookResource(Resource):
@marshal_with(book_fields)
def get(self, book_id):
return Book.query.get_or_404(book_id)
@marshal_with(book_fields)
def put(self, book_id):
book = Book.query.get_or_404(book_id)
args = book_parser.parse_args()
book.title = args['title']
book.author = args['author']
book.price = args['price']
db.session.commit()
return book
def delete(self, book_id):
book = Book.query.get_or_404(book_id)
db.session.delete(book)
db.session.commit()
return '', 204
# Register routes
api.add_resource(BookList, '/api/books')
api.add_resource(BookResource, '/api/books/')
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
FastAPI: Modern, Fast, and Auto-Documented
FastAPI is a modern framework specifically designed for building APIs. It's incredibly fast and generates automatic API documentation.
# Install
pip install fastapi uvicorn sqlalchemy pydantic
# main.py
from fastapi import FastAPI, HTTPException, Depends
from sqlalchemy import create_engine, Column, Integer, String, Float
from sqlalchemy.orm import sessionmaker, Session, declarative_base
from pydantic import BaseModel, validator
# Database setup
DATABASE_URL = "sqlite:///./books.db"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)
Base = declarative_base()
# SQLAlchemy Model
class BookDB(Base):
__tablename__ = "books"
id = Column(Integer, primary_key=True)
title = Column(String, nullable=False)
author = Column(String, nullable=False)
price = Column(Float, nullable=False)
Base.metadata.create_all(bind=engine)
# Pydantic models (for validation and serialization)
class BookBase(BaseModel):
title: str
author: str
price: float
@validator('price')
def price_must_be_positive(cls, v):
if v <= 0:
raise ValueError('Price must be positive')
return v
class BookCreate(BookBase):
pass
class Book(BookBase):
id: int
class Config:
from_attributes = True
# FastAPI app
app = FastAPI(title="Book API", version="1.0.0")
# Dependency
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Endpoints
@app.get("/api/books", response_model=list[Book])
def get_books(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
"""Get all books with pagination"""
books = db.query(BookDB).offset(skip).limit(limit).all()
return books
@app.get("/api/books/{book_id}", response_model=Book)
def get_book(book_id: int, db: Session = Depends(get_db)):
"""Get a specific book by ID"""
book = db.query(BookDB).filter(BookDB.id == book_id).first()
if not book:
raise HTTPException(status_code=404, detail="Book not found")
return book
@app.post("/api/books", response_model=Book, status_code=201)
def create_book(book: BookCreate, db: Session = Depends(get_db)):
"""Create a new book"""
db_book = BookDB(**book.dict())
db.add(db_book)
db.commit()
db.refresh(db_book)
return db_book
@app.put("/api/books/{book_id}", response_model=Book)
def update_book(book_id: int, book: BookCreate, db: Session = Depends(get_db)):
"""Update a book"""
db_book = db.query(BookDB).filter(BookDB.id == book_id).first()
if not db_book:
raise HTTPException(status_code=404, detail="Book not found")
for key, value in book.dict().items():
setattr(db_book, key, value)
db.commit()
db.refresh(db_book)
return db_book
@app.delete("/api/books/{book_id}", status_code=204)
def delete_book(book_id: int, db: Session = Depends(get_db)):
"""Delete a book"""
db_book = db.query(BookDB).filter(BookDB.id == book_id).first()
if not db_book:
raise HTTPException(status_code=404, detail="Book not found")
db.delete(db_book)
db.commit()
return None
# Run with: uvicorn main:app --reload
# Visit http://127.0.0.1:8000/docs for auto-generated docs!
# Visit http://127.0.0.1:8000/redoc for alternative docs
FastAPI: Advanced Features
# Authentication with JWT
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from datetime import datetime, timedelta
import jwt
security = HTTPBearer()
SECRET_KEY = "your-secret-key"
def create_access_token(data: dict):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(hours=1)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm="HS256")
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
try:
payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=["HS256"])
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
@app.post("/api/login")
def login(username: str, password: str):
# Validate credentials (simplified)
if username == "admin" and password == "secret":
token = create_access_token({"sub": username})
return {"access_token": token, "token_type": "bearer"}
raise HTTPException(status_code=401, detail="Invalid credentials")
@app.get("/api/protected")
def protected_route(user = Depends(verify_token)):
return {"message": f"Hello {user['sub']}"}
# Background tasks
from fastapi import BackgroundTasks
def send_email(email: str, message: str):
# Simulate sending email
print(f"Sending email to {email}: {message}")
@app.post("/api/send-notification")
def send_notification(email: str, background_tasks: BackgroundTasks):
background_tasks.add_task(send_email, email, "Your book order confirmed!")
return {"message": "Notification will be sent"}
# File uploads
from fastapi import File, UploadFile
@app.post("/api/upload")
async def upload_file(file: UploadFile = File(...)):
contents = await file.read()
# Process file
return {"filename": file.filename, "size": len(contents)}
# CORS (for frontend integration)
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # React app
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Choosing the Right Framework
Use Django REST Framework when:
- Building a full-featured web app with admin panel
- Need Django's ecosystem (ORM, auth, admin)
- Working with an existing Django project
- Team is already familiar with Django
- Need robust, battle-tested solution
Use Flask-RESTful when:
- Building a simple API without heavy framework
- Want flexibility and minimalism
- Already using Flask for your project
- Need lightweight microservices
Use FastAPI when:
- Building modern, high-performance APIs
- Want automatic API documentation
- Need async support for better performance
- Type hints and data validation are priorities
- Building microservices or standalone APIs
API Design Best Practices
- Use nouns for resources:
/api/booksnot/api/getBooks - Use plural names:
/api/usersnot/api/user - Nest resources logically:
/api/users/1/postsfor user's posts - Version your API:
/api/v1/booksfor future changes - Use query params for filtering:
/api/books?author=tolkien - Implement pagination: Don't return 10,000 items at once
- Return appropriate status codes: 200, 201, 400, 404, 500
- Provide meaningful error messages: Not just "Error"
- Use HTTPS in production: Always encrypt API traffic
- Document your API: Use Swagger/OpenAPI (FastAPI does this automatically)
- Rate limiting: Prevent abuse with throttling
- CORS headers: Allow frontend apps to access your API
Testing APIs
# Using pytest with FastAPI
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_get_books():
response = client.get("/api/books")
assert response.status_code == 200
assert isinstance(response.json(), list)
def test_create_book():
response = client.post("/api/books", json={
"title": "Test Book",
"author": "Test Author",
"price": 19.99
})
assert response.status_code == 201
assert response.json()["title"] == "Test Book"
# Using pytest with DRF
from rest_framework.test import APITestCase
class BookAPITest(APITestCase):
def test_get_books(self):
response = self.client.get('/api/books/')
self.assertEqual(response.status_code, 200)
def test_create_book(self):
data = {'title': 'Test Book', 'author': 'Author', 'price': 19.99}
response = self.client.post('/api/books/', data)
self.assertEqual(response.status_code, 201)
# Manual testing with curl
curl http://localhost:8000/api/books
curl -X POST http://localhost:8000/api/books \
-H "Content-Type: application/json" \
-d '{"title": "New Book", "author": "Alice", "price": 29.99}'
# Or use Postman, Insomnia, or HTTPie for easier testing
Master API Development with Expert Mentorship
Our Full Stack Python program covers REST API development with Django REST Framework, Flask, and FastAPI. Learn API design, authentication, testing, and deployment with real-world projects.
Explore Full Stack Python Program