A New Life for aiohttp?

Introduction
Once upon a time, aiohttp
was a revolution in the Python world: the first asynchronous web framework that allowed developers to build fast, efficient, and truly ergonomic web applications using asyncio
.
Years have passed, and the world has changed. We’ve seen the rise of FastAPI, Pydantic, Litestar, Starlette — all striving to make async development simpler, safer, and more powerful. But one thing hasn’t changed: aiohttp is still the foundation of the Python async ecosystem.
And today we’re introducing Rapidy — a new framework inspired by the best ideas of aiohttp, reimagined for 2025.
TL;DR
Rapidy is a modern asynchronous Python framework that rethinks the ideas of aiohttp.
It combines strict typing, support for both Pydantic v1/v2, DI through Dishka, mypy
compatibility, and remains lightweight, minimalist, and backward-compatible with aiohttp.
🔄 aiohttp compatible
✅ Full
mypy
, Pydantic, and DI support🧩 Simplified routing, controllers, middleware
🧰 Lifecycle hooks, content-type filtering
🔧 Production-ready and open to contributors
Docs and code:
📚 Docs: https://rapidy.dev
⭐ GitHub: https://github.com/rAPIdy-org/rAPIdy
What can Rapidy do?
Native support for Pydantic v1 and v2 — validation and serialization out of the box.
Validate and serialize directly in handlers, and yes — you can use middleware for that too:
from pydantic import BaseModel
from rapidy import Rapidy
from rapidy.http import Body, Request, Header, StreamResponse, middleware, post
from rapidy.typedefs import CallNext
TOKEN_REGEXP = '^[Bb]earer (?P<token>[A-Za-z0-9-_=.]*)'
class RequestBody(BaseModel):
username: str
password: str
class ResponseBody(BaseModel):
hello: str = 'rapidy'
@middleware
async def get_bearer_middleware(
request: Request,
call_next: CallNext,
bearer_token: str = Header(alias='Authorization', pattern=TOKEN_REGEXP),
) -> StreamResponse:
# process token here ...
return await call_next(request)
@post('/')
async def handler(
body: RequestBody = Body(),
) -> ResponseBody:
return ResponseBody()
app = Rapidy(
middlewares=[get_bearer_middleware],
http_route_handlers=[handler],
)
Full mypy
and strict typing support — Rapidy comes with its own mypy plugin:
# pyproject.toml
[tool.mypy]
plugins = [
"pydantic.mypy",
"rapidy.mypy" # <-- enable rapidy plugin
]
Native integration with Dishka
, one of the most elegant DI frameworks in the Python world.
https://github.com/reagento/dishka
Rapidy provides a Dependency Container with clear and predictable behavior:
from rapidy import Rapidy
from rapidy.http import get
from rapidy.depends import FromDI, provide, Provider, Scope
class HelloProvider(Provider):
@provide(scope=Scope.REQUEST)
async def hello(self) -> str:
return 'hello, Rapidy!'
@get('/')
async def handler(hello: FromDI[str]) -> dict:
return {"message": hello}
app = Rapidy(
http_route_handlers=[handler],
di_providers=[HelloProvider()],
)
Full backward compatibility with existing aiohttp code:
from rapidy import web
from rapidy.http import HTTPRouter, get
from pydantic import BaseModel
# --- old functionality
routes = web.RouteTableDef()
@routes.get('/user')
async def get_user_aiohttp(request: web.Request) -> web.Response:
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
app = web.Application(
http_route_handlers=[v2_router],
)
app.add_subapp('/v1', v1_app)
You can even use Rapidy's functionality inside your old code — pure magic.
Migration guide: https://rapidy.dev/latest/aiohttp_migration/
Controllers — enhanced class-based views:
from rapidy import Rapidy
from rapidy.http import PathParam, controller, get, post, put, patch, delete
@controller('/')
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 [{'name': 'John'}, {'name': 'Felix'}]
@post()
async def create_user(self) -> str:
return 'ok'
rapidy = Rapidy(http_route_handlers=[UserController])
Better and simpler routing — say goodbye to awkward Application
nesting:
from rapidy import Rapidy
from rapidy.http import HTTPRouter, controller, get
@get('/hello')
async def hello_handler() -> dict[str, str]:
return {'hello': 'rapidy'}
@controller('/hello_controller')
class HelloController:
@get()
async def get_hello(self) -> dict[str, str]:
return {'hello': 'rapidy'}
api_router = HTTPRouter('/api', [hello_handler, HelloController])
rapidy = Rapidy(http_route_handlers=[api_router])
Convenient lifespan management — control the app lifecycle your way:
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from rapidy import Rapidy
def hello() -> None:
print('hello')
@asynccontextmanager
async def bg_task() -> AsyncGenerator[None, None]:
try:
print('starting background task')
yield
finally:
print('finishing background task')
rapidy = Rapidy(
lifespan=[bg_task()],
on_startup=[hello],
on_shutdown=[hello],
on_cleanup=[hello],
)
Unified Body
extraction and full control over accepted content types:
from pydantic import BaseModel
from rapidy.http import post, Body, ContentType
class UserData(BaseModel):
username: str
password: str
@post('/')
async def handler_json(
user_data: UserData = Body(),
# or
user_data: UserData = Body(content_type=ContentType.json),
# or
user_data: UserData = Body(content_type='application/json'),
) -> ...:
@post('/')
async def handler_x_www(
user_data: UserData = Body(content_type=ContentType.x_www_form),
# or
user_data: UserData = Body(content_type='application/x-www-form-urlencoded'),
) -> ...:
@post('/')
async def handler(
user_data: UserData = Body(content_type=ContentType.m_part_form_data),
# or
user_data: UserData = Body(content_type='multipart/form-data'),
) -> ...:
# ... and others ...
Full and readable documentation is available at:
https://rapidy.dev
Rapidy includes much more than shown here — explore more features in the docs or future posts.
⁉️ Why Another Framework?
Because Python evolves.
In 2016, aiohttp was a breakthrough.
In 2020, FastAPI showed how expressive and declarative an API could be.
In 2025, Rapidy proves you can have it all — without compromise.
💡 Why Contribute to Rapidy?
Rapidy was built with care and attention to detail. Its modular architecture, clean structure, and comprehensive internal documentation are designed to make contributing as easy and pleasant as possible.
I’ve worked hard to make the codebase friendly and accessible: clearly separated layers, zero magic, and transparent component mechanics.
If you’ve ever wanted to contribute to a framework but felt intimidated — Rapidy is the project where you’ll feel confident from the very beginning.
We also welcome discussions, ideas, and feedback. This is not just a repo — it’s a community where anyone can shape the future of modern Python development.
🚀 What’s Next?
OpenAPI is almost done and will be released very soon.
Boilerplates for fast microservice scaffolding are on the way.
Drafts for WebSocket and gRPC support are in progress.
📌 In Summary
This project is not an attempt to replace aiohttp. It's an attempt to give it a second life — with the same spirit of simplicity, the same respect for clarity and control, but with the features modern development demands.
And we would be truly honored if Andrey Svetlov — the person who started it all — joined us on this journey.
Try It Out
The project is open and already in active use:
👉 https://github.com/rAPIdy-org/rAPIdy
Docs, examples, discussions — all open.
If you once loved aiohttp but have been waiting for something fresh — Rapidy is here.
Subscribe to my newsletter
Read articles from Daniil Grois directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
