A lightweight, package-based routing framework for building RESTful APIs in Python.
Courier eliminates boilerplate routing code by automatically mapping your Python package structure to URL routes. Organize your API endpoints into a logical package hierarchy, and Courier handles the rest.
- 🗂️ Package-Based Routing - Your package structure becomes your URL structure
- 🔄 Automatic Route Discovery - No manual route registration required
- ✅ Built-in Validation - Request schema validation using Voluptuous
- 🔐 Token Authentication - Cryptographic signing with itsdangerous
- 🌐 CORS Support - Built-in CORS headers and OPTIONS handling
- 🔀 Parameter Transformation - Automatic snake_case ↔ camelCase conversion
- ⚡ Powered by Werkzeug - Battle-tested WSGI routing from the Flask team
- 🧪 Fully Tested - Comprehensive test suite with CI/CD
pip install courierOr install from source:
git clone https://github.com/cwaeland/courier.git
cd courier
pip install -e .my_project/
├── app.py
└── api/
├── __init__.py
└── users.py
api/users.py:
from courier.routing.api_method import APIMethod
from voluptuous import Schema, Required
class Users(APIMethod):
"""Handle user collection endpoints"""
@staticmethod
@APIMethod.public(
http_method="get",
requires_auth=False
)
def get(self, request):
"""List all users"""
return {
"users": [
{"user_id": 1, "user_name": "Alice"},
{"user_id": 2, "user_name": "Bob"}
]
}
@staticmethod
@APIMethod.public(
http_method="post",
api_schema=Schema({
Required("userName"): str,
Required("email"): str
}),
requires_auth=False
)
def post(self, request):
"""Create a new user"""
params = request["params"]
return {
"user_id": 3,
"user_name": params["user_name"],
"email": params["email"]
}
class User(APIMethod):
"""Handle individual user endpoints"""
@staticmethod
def is_detail():
return True
@staticmethod
def get_base_schema():
return {"userId": int}
@staticmethod
@APIMethod.public(http_method="get", requires_auth=False)
def get(self, request):
"""Get a specific user"""
user_id = request["params"]["user_id"]
return {
"user_id": user_id,
"user_name": f"User {user_id}"
}app.py:
from courier import Courier
import os
# Create Courier app
app = Courier(
web_root_path=os.path.join(os.path.dirname(__file__), "api"),
web_root_name="api"
)
if __name__ == "__main__":
from werkzeug.serving import run_simple
run_simple('localhost', 5000, app, use_debugger=True, use_reloader=True)python app.pyYour API is now running at http://localhost:5000!
# List all users
curl http://localhost:5000/users/
# Get specific user
curl http://localhost:5000/users/1/
# Create new user
curl -X POST http://localhost:5000/users/ \
-H "Content-Type: application/json" \
-d '{"userName": "Charlie", "email": "charlie@example.com"}'
# View all routes
curl http://localhost:5000/routesCourier maps your package structure to URL paths:
api/
├── users.py → /users/
├── posts.py → /posts/
└── admin/
└── settings.py → /admin/settings/
Classes with is_detail() = True create parameterized routes:
class User(APIMethod):
@staticmethod
def is_detail():
return True
@staticmethod
def get_base_schema():
return {"userId": int} # Defines the parameter nameThis creates routes like /users/{userId}/
Create hierarchical APIs by nesting packages:
api/
└── users/
├── __init__.py # Users collection
└── posts/
└── __init__.py # User's posts
Results in routes:
/users/- User collection/users/{userId}/- Specific user/users/{userId}/posts/- User's posts/users/{userId}/posts/{postId}/- Specific post
-
Inbound: camelCase parameters → snake_case
{"userName": "Alice"} → {"user_name": "Alice"} -
Processing: Your method logic runs
-
Outbound: snake_case response → camelCase JSON
{"user_name": "Alice"} → {"userName": "Alice"} -
Response Format:
{ "status": "success", "code": null, "message": null, "signature": null, "return_value": { "userName": "Alice" } }
Use Voluptuous schemas to validate request parameters:
from voluptuous import Schema, Required, Email, Range
@APIMethod.public(
http_method="post",
api_schema=Schema({
Required("userName"): str,
Required("email"): Email(),
Required("age"): Range(min=18, max=120)
})
)
def post(self, request):
# Parameters are already validated!
params = request["params"]
return {"user_created": True}Invalid requests automatically return a 422 error with details.
Add token-based authentication:
from courier.routing.authenticator import APIAuthenticator
from itsdangerous import TimestampSigner
# Create authenticator
signer = TimestampSigner("your-secret-key")
auth = APIAuthenticator(signer, expiration=3600)
class ProtectedResource(APIMethod):
@staticmethod
def get_default_auth():
return auth
@staticmethod
@APIMethod.public(http_method="get", requires_auth=True)
def get(self, request):
# Access authenticated user
user_token = self.user_token
return {"message": f"Hello {user_token}!"}Create reusable validators:
from voluptuous import Invalid
import re
def Username(value):
"""Validate username format"""
if not re.match(r'^[a-zA-Z0-9_]{3,20}$', value):
raise Invalid('Username must be 3-20 alphanumeric characters or underscores')
return value
# Use in schema
api_schema=Schema({
Required("userName"): Username
})Courier provides structured error responses:
from courier.routing.exc import APIError
@APIMethod.public(http_method="get")
def get(self, request):
user_id = request["params"]["user_id"]
if user_id > 1000:
raise APIError("UserNotFound", "User does not exist")
return {"user_id": user_id}Returns:
{
"status": "error",
"code": "UserNotFound",
"message": "User does not exist",
"return_value": null
}Handle custom types in responses:
class MyAPI(APIMethod):
@staticmethod
def get_extra_json_types():
return [
{
"instance": MyCustomClass,
"callable": lambda api_obj, obj: obj.to_dict()
}
]Implement custom access control:
@staticmethod
def process_acl(user_token, params, **add_params):
# Check if user has permission
if not user_has_permission(user_token, params):
raise APIError("Forbidden", "Access denied")
return {}# Install development dependencies
pip install -r requirements-dev.txt
# Run tests
pytest
# Run with coverage
pytest --cov=courier --cov-report=html# Format code
black courier/ tests/
# Lint code
ruff check courier/ tests/
# Type checking
mypy courier/- Python 3.8+
- Werkzeug >= 3.0.0
- Voluptuous >= 0.14.2
- itsdangerous >= 2.2.0
Deploy with any WSGI server:
pip install gunicorn
gunicorn app:apppip install uwsgi
uwsgi --http :5000 --wsgi-file app.py --callable apppip install waitress
waitress-serve --port=5000 app:appContributions are welcome! Please see CONTRIBUTING.md for guidelines.
See CHANGES.md for version history.
MIT License - see LICENSE.md for details.
Created by Cameron Waeland
Modernized and maintained by the community
Built with ❤️ using Werkzeug, Voluptuous, and itsdangerous