Migration to Rapidy from aiohttp
Description
rapidy carefully extends aiohttp, which means that all code written for aiohttp will work without modifications.
rapidy uses the same module names as aiohttp.
If the required object is not available in rapidy, it can be imported directly from aiohttp.
aiohttp issues and their solutions in Rapidy
The aiohttp framework has a number of features and limitations that Rapidy addresses in a more convenient and elegant way.
Simplified handling of the request parameter
In aiohttp, route handlers require an explicit request parameter, even if it is not used.
Example (aiohttp):
from aiohttp import web
router = web.RouteTableDef()
@router.get('/hello')
async def hello(request: web.Request) -> web.Response: # `request` required
return web.json_response({'hello': 'aiohttp'})
app = web.Application()
app.add_routes(router)
In Rapidy, the request parameter is not required, making the code more concise:
Example (Rapidy):
from rapidy import Rapidy
from rapidy.http import get
@get('/hello')
async def handler() -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = Rapidy(http_route_handlers=[handler])
Learn more about Request handling here.
Simplified background task handling
In aiohttp, background task handlers require an explicit app parameter:
from typing import AsyncGenerator
from aiohttp import web
async def bg_task(app: web.Application) -> None:
print('run task')
async def app_ctx(app: web.Application) -> AsyncGenerator[None, None]:
print('starting background task')
yield
print('finish')
app = web.Application()
app.on_startup.append(bg_task)
app.on_shutdown.append(bg_task)
app.on_cleanup.append(bg_task)
app.cleanup_ctx.append(app_ctx)
In Rapidy, the app parameter is not required:
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from rapidy import Rapidy
async def bg_task() -> None:
print('run task')
@asynccontextmanager
async def app_ctx() -> AsyncGenerator[None, None]:
print('starting background task')
yield
print('finishing background task')
rapidy = Rapidy(
on_startup=[bg_task],
on_shutdown=[bg_task],
on_cleanup=[bg_task],
lifespan=[app_ctx()],
)
# or
rapidy.lifespan.on_startup.append(bg_task)
rapidy.lifespan.on_shutdown.append(bg_task)
rapidy.lifespan.on_cleanup.append(bg_task)
rapidy.lifespan.append(app_ctx())
The app attribute can still be passed if needed, learn more here.
Learn more about background task handling here.
Improved class-based handler routing
In aiohttp, class-based handler routing is tied to HTTP method names, limiting flexibility.
In Rapidy, handlers can be conveniently grouped:
from rapidy import Rapidy
from rapidy.http import controller, get, PathParam
@controller('/user')
class UserController:
@get('/{user_id}')
async def get_by_id(self, user_id: str = PathParam()) -> dict[str, str]:
return {'user_id': user_id}
@get()
async def get_all_users(self) -> list[dict[str, str]]:
return [{'user_id': '1'}, {'user_id': '2'}]
rapidy = Rapidy(http_route_handlers=[UserController])
Learn more about HTTP handler functionality here.
Simple data validation and serialization
In Rapidy, request data can be easily validated and serialized using pydantic:
from pydantic import BaseModel
from rapidy.http import get, Body
class UserData(BaseModel):
name: str
age: int
@get('/user')
async def handler(data: UserData = Body()) -> dict[str, str]:
return {'message': f'User {data.name}, {data.age} years old'}
Simpler Application Modularization
In aiohttp, adding sub-applications requires explicit route additions and Application object initialization:
from aiohttp import web
v1_app = web.Application()
app = web.Application()
app.add_subapp('/v1', v1_app)
In Rapidy, this process is simpler:
from rapidy import Rapidy
from rapidy.http import HTTPRouter
v1_app = HTTPRouter('/v1')
rapidy = Rapidy(http_route_handlers=[v1_app])
Key Advantages of Rapidy
- More concise code: Reduces boilerplate without sacrificing readability.
- Convenient routing: Allows grouping handlers within classes.
- Flexible data validation and serialization: Built-in
pydanticsupport. - Simplified request and response handling: Access
RequestandResponsevia type annotations. - Advanced application lifecycle management: Easy-to-use
on_startup,on_shutdown, andon_cleanuphooks. - Better integration with Python’s async capabilities: Fewer unnecessary parameters and less manual data handling.
Learn more about Rapidy features here.
How to migrate from aiohttp to Rapidy
Migrating from aiohttp to Rapidy simplifies code, removes boilerplate, improves validation, and makes dependency management easier.
Full migration
Suppose you have an aiohttp application with a request handler, request parameters, validation, and lifecycle hooks:
Code in aiohttp:
from aiohttp import web
from pydantic import BaseModel, ValidationError
routes = web.RouteTableDef()
class UserData(BaseModel):
name: str
age: int
@routes.post('/user')
async def create_user(request: web.Request) -> web.Response:
data = await request.json()
try:
user = UserData(**data)
except ValidationError as validation_err:
return web.json_response({'error': validation_err.errors()}, status=400)
return web.json_response({'message': f'User {user.name}, {user.age} years old'})
async def on_startup(app: web.Application) -> None:
print("App is starting...")
app = web.Application()
app.on_startup.append(on_startup)
app.add_routes(routes)
web.run_app(app)
Code in Rapidy:
from rapidy import Rapidy
from rapidy.http import post, Body
from pydantic import BaseModel
class UserData(BaseModel):
name: str
age: int
@post('/user')
async def create_user(user: UserData = Body()) -> dict[str, str]:
return {'message': f'User {user.name}, {user.age} years old'}
async def on_startup() -> None:
print("App is starting...")
rapidy = Rapidy(
http_route_handlers=[create_user],
on_startup=[on_startup],
)
Key improvements:
- Less boilerplate — No explicit
request: web.Request,Rapidyautomatically parses and validates JSON usingpydantic. - Simplified lifecycle management —
on_startupis passed as a list instead of usingapp.on_startup.append(). - Routing without
app.add_routes()— Handlers are provided viahttp_route_handlers.
Partial migration
Sometimes a full migration is not immediately possible, such as when new functionality needs to be added while retaining some aiohttp code.
In this case, you can transition gradually.
This approach allows you to migrate code to Rapidy by replacing parts of aiohttp with the new syntax without rewriting the entire project.
Adding small features
Suppose you need to retrieve the Host header and include it in the response without changing the rest of the logic.
Before (aiohttp):
from aiohttp import web
from pydantic import BaseModel, ValidationError
routes = web.RouteTableDef()
class UserData(BaseModel):
name: str
age: int
@routes.post('/user')
async def create_user(
request: web.Request,
) -> web.Response:
data = await request.json()
try:
user = UserData(**data)
except ValidationError as validation_err:
return web.json_response({'error': validation_err.errors()}, status=400)
return web.json_response({'message': f'User {user.name}, {user.age} years old'})
async def on_startup(app: web.Application) -> None:
print("App is starting...")
app = web.Application()
app.on_startup.append(on_startup)
app.add_routes(routes)
After (Rapidy):
from rapidy import Rapidy, web
from pydantic import BaseModel, ValidationError
routes = web.RouteTableDef()
class UserData(BaseModel):
name: str
age: int
@routes.post('/user')
async def create_user(
request: web.Request,
host: str = web.Header(alias='Host'),
) -> web.Response:
data = await request.json()
try:
user = UserData(**data)
except ValidationError as validation_err:
return web.Response({'error': validation_err.errors()}, status=400)
return web.Response({'message': f'User {user.name}, {user.age} years old, host: {host}'})
async def on_startup(app: web.Application) -> None:
print("App is starting...")
app = web.Application()
app.on_startup.append(on_startup)
app.add_routes(routes)
Changes:
- Updated imports.
- Added
host: str = web.Header(alias='Host')in thecreate_userHTTP handler. - Replaced
json_responsewithResponse, as its functionality is now built intoResponse.
Response in Rapidy offers more features, learn more here.
Rapidy can do (almost) everything
The example above shows how to extend code using web.RouteTableDef(),
but Rapidy also supports all other aiohttp handler declaration styles.
Learn more about aiohttp-style handlers here.
Adding new functionality via sub-applications
You can easily extend an existing aiohttp application by adding new functionality through sub-applications.
Before (aiohttp):
from aiohttp import web
from pydantic import BaseModel
routes = web.RouteTableDef()
class UserData(BaseModel):
name: str
age: int
@routes.post('/user')
async def create_user(request: web.Request) -> web.Response:
# ... some aiohttp code
return web.Response(text='User')
v1_app = web.Application()
v1_app.add_routes(routes)
app = web.Application()
app.add_subapp('/v1', v1_app)
After (Rapidy):
from rapidy import web
from rapidy.http import HTTPRouter, get
from pydantic import BaseModel
routes = web.RouteTableDef()
class UserData(BaseModel):
name: str
age: int
@routes.get('/user')
async def get_user_aiohttp(request: web.Request) -> web.Response:
# ... some aiohttp code
return web.Response(text='User aiohttp')
v1_app = web.Application()
v1_app.add_routes(routes)
# --- new functionality
@get('/user')
async def get_user_rapidy() -> str:
return 'User rapidy'
v2_router = HTTPRouter('/v2', route_handlers=[get_user_rapidy])
# ---
app = web.Application(http_route_handlers=[v2_router])
app.add_subapp('/v1', v1_app)
This approach enables seamless integration of Rapidy into existing code without requiring significant modifications.
However, you will need to update aiohttp imports to rapidy and replace json_response with Response throughout your code.