Organizing Routers in Python Web Frameworks: APIRouter vs Flask Blueprint vs Django URLconf

APIRouter vs Flask Blueprint vs Django URLconf

As your Python web application grows, keeping all routes in a single file becomes unmanageable. Each major Python web framework provides a solution for modular route organization:

  • FastAPI: APIRouter
  • Flask: Blueprint
  • Django: URL includes and apps

Let’s explore each approach and see how they compare.

The Problem: Monolithic Routes

Imagine having 50+ endpoints in a single file:

# This quickly becomes unmanageable
@app.get("/users/")
@app.get("/users/{id}")
@app.post("/users/")
@app.get("/products/")
@app.get("/products/{id}")
@app.post("/orders/")
# ... 50 more routes

All three frameworks solve this with modular route organization.


FastAPI: APIRouter

FastAPI’s APIRouter works exactly like the main FastAPI app but allows you to define routes in separate modules.

Basic Structure

app/
├── main.py
├── routers/
│ ├── __init__.py
│ ├── users.py
│ └── products.py

Creating a Router

# routers/users.py
from fastapi import APIRouter, HTTPException

router = APIRouter(
prefix="/users",
tags=["users"],
responses={404: {"description": "Not found"}},
)

@router.get("/")
async def list_users():
return [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]

@router.get("/{user_id}")
async def get_user(user_id: int):
return {"id": user_id, "name": "Alice"}

@router.post("/")
async def create_user(name: str):
return {"id": 3, "name": name}

Including Routers

# main.py
from fastapi import FastAPI
from routers import users, products

app = FastAPI(title="My API")

app.include_router(users.router)
app.include_router(products.router)

# You can also add prefix at include time
app.include_router(
admin.router,
prefix="/admin",
tags=["admin"],
)

Key Features

  • Prefix: All routes get a common URL prefix
  • Tags: Groups endpoints in OpenAPI docs
  • Dependencies: Apply dependencies to all routes in router
  • Responses: Define common responses
from fastapi import Depends

router = APIRouter(
prefix="/items",
tags=["items"],
dependencies=[Depends(verify_token)], # Applied to all routes
)

Flask: Blueprint

Flask’s Blueprint predates APIRouter and served as its inspiration. It provides similar functionality for organizing routes.

Basic Structure

app/
├── __init__.py
├── users/
│ ├── __init__.py
│ └── routes.py
└── products/
├── __init__.py
└── routes.py

Creating a Blueprint

# users/routes.py
from flask import Blueprint, jsonify

users_bp = Blueprint('users', __name__, url_prefix='/users')

@users_bp.route('/')
def list_users():
return jsonify([{"id": 1, "name": "Alice"}])

@users_bp.route('/<int:user_id>')
def get_user(user_id):
return jsonify({"id": user_id, "name": "Alice"})

@users_bp.route('/', methods=['POST'])
def create_user():
return jsonify({"id": 3, "name": "New User"}), 201

Registering Blueprints

# __init__.py
from flask import Flask

def create_app():
app = Flask(__name__)

from .users.routes import users_bp
from .products.routes import products_bp

app.register_blueprint(users_bp)
app.register_blueprint(products_bp)

# Override prefix at registration
app.register_blueprint(admin_bp, url_prefix='/admin')

return app

Blueprint Features

  • URL Prefix: Common prefix for all routes
  • Static Files: Blueprint-specific static folder
  • Templates: Blueprint-specific template folder
  • Error Handlers: Blueprint-scoped error handling
users_bp = Blueprint(
'users',
__name__,
url_prefix='/users',
template_folder='templates',
static_folder='static',
)

@users_bp.errorhandler(404)
def not_found(error):
return jsonify({"error": "User not found"}), 404

Django: URLconf and Apps

Django takes a different approach with its app-based architecture. Each app contains its own urls.py which is included in the main URLconf.

Basic Structure

project/
├── project/
│ ├── __init__.py
│ ├── settings.py
│ └── urls.py # Main URLconf
├── users/
│ ├── __init__.py
│ ├── urls.py # App URLs
│ └── views.py
└── products/
├── __init__.py
├── urls.py
└── views.py

Creating App URLs

# users/views.py
from django.http import JsonResponse
from django.views import View

def list_users(request):
return JsonResponse([{"id": 1, "name": "Alice"}], safe=False)

def get_user(request, user_id):
return JsonResponse({"id": user_id, "name": "Alice"})

class UserCreateView(View):
def post(self, request):
return JsonResponse({"id": 3, "name": "New User"}, status=201)
# users/urls.py
from django.urls import path
from . import views

app_name = 'users' # Namespace

urlpatterns = [
path('', views.list_users, name='list'),
path('<int:user_id>/', views.get_user, name='detail'),
path('create/', views.UserCreateView.as_view(), name='create'),
]

Including App URLs

# project/urls.py
from django.urls import path, include

urlpatterns = [
path('users/', include('users.urls')),
path('products/', include('products.urls')),
path('api/v1/', include([
path('users/', include('users.urls')),
path('products/', include('products.urls')),
])),
]

Django REST Framework Routers

For REST APIs, Django REST Framework provides automatic URL routing:

# users/views.py
from rest_framework import viewsets
from .models import User
from .serializers import UserSerializer

class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
# users/urls.py
from rest_framework.routers import DefaultRouter
from .views import UserViewSet

router = DefaultRouter()
router.register(r'users', UserViewSet)

urlpatterns = router.urls

This automatically creates:

  • GET /users/ - List
  • POST /users/ - Create
  • GET /users/{id}/ - Retrieve
  • PUT /users/{id}/ - Update
  • DELETE /users/{id}/ - Delete

Side-by-Side Comparison

Feature FastAPI APIRouter Flask Blueprint Django URLconf
URL Prefix prefix="/users" url_prefix='/users' path('users/', include(...))
Grouping tags=["users"] Blueprint name app_name namespace
Dependencies dependencies=[...] @bp.before_request Middleware/decorators
Auto Documentation Yes (OpenAPI) No (needs extensions) DRF has browsable API
Nested Routers Via include Via nested blueprints Via nested includes
Static Files No Yes Via app static folders
Templates No Yes Via app template folders

Practical Example: User Management Module

FastAPI

# routers/users.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from ..database import get_db
from .. import schemas, crud

router = APIRouter(prefix="/users", tags=["users"])

@router.get("/", response_model=list[schemas.User])
def list_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
return crud.get_users(db, skip=skip, limit=limit)

@router.post("/", response_model=schemas.User, status_code=201)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
db_user = crud.get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
return crud.create_user(db=db, user=user)

Flask

# users/routes.py
from flask import Blueprint, request, jsonify
from ..models import User, db

users_bp = Blueprint('users', __name__, url_prefix='/users')

@users_bp.route('/')
def list_users():
skip = request.args.get('skip', 0, type=int)
limit = request.args.get('limit', 100, type=int)
users = User.query.offset(skip).limit(limit).all()
return jsonify([u.to_dict() for u in users])

@users_bp.route('/', methods=['POST'])
def create_user():
data = request.get_json()
if User.query.filter_by(email=data['email']).first():
return jsonify({"error": "Email already registered"}), 400
user = User(**data)
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201

Django REST Framework

# users/views.py
from rest_framework import viewsets, status
from rest_framework.response import Response
from .models import User
from .serializers import UserSerializer

class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer

def create(self, request):
if User.objects.filter(email=request.data.get('email')).exists():
return Response(
{"error": "Email already registered"},
status=status.HTTP_400_BAD_REQUEST
)
return super().create(request)

When to Use Each

Choose FastAPI APIRouter when:

  • Building modern async APIs
  • Need automatic OpenAPI documentation
  • Want type hints and validation
  • Performance is critical

Choose Flask Blueprint when:

  • Building traditional web apps with templates
  • Need blueprint-specific static files
  • Prefer simplicity and flexibility
  • Have existing Flask ecosystem

Choose Django URLconf when:

  • Building full-featured web applications
  • Need Django’s ORM and admin
  • Want DRF’s powerful features
  • Enterprise/large team projects

Conclusion

All three approaches solve the same problem: organizing routes in a modular, maintainable way. The choice depends on your framework preference and project requirements:

  • FastAPI’s APIRouter is the most modern, with excellent async support and auto-documentation
  • Flask’s Blueprint is battle-tested and flexible, great for traditional web apps
  • Django’s URLconf integrates with Django’s app architecture, ideal for full-stack applications

Whichever you choose, the key principle remains: keep related routes together and separate concerns into logical modules.

Further Reading


   Reprint policy


《Organizing Routers in Python Web Frameworks: APIRouter vs Flask Blueprint vs Django URLconf》 by Isaac Zhou is licensed under a Creative Commons Attribution 4.0 International License
 Previous
Richardson Maturity Model and HATEOAS - Building Truly RESTful APIs with FastAPI Richardson Maturity Model and HATEOAS - Building Truly RESTful APIs with FastAPI
Understanding the four levels of REST API maturity and implementing HATEOAS in Python with FastAPI for self-documenting, discoverable APIs.
2026-01-07
Next 
Testing in Python - Django vs Flask vs FastAPI Testing in Python - Django vs Flask vs FastAPI
A comprehensive comparison of testing libraries and approaches for Django, Flask, and FastAPI - from unit tests to integration tests.
2026-01-05
  TOC