A New Life for aiohttp?

Daniil GroisDaniil Grois
5 min read

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.

1
Subscribe to my newsletter

Read articles from Daniil Grois directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Daniil Grois
Daniil Grois