Skip to content

ndugram/fasthttp

Fast, simple HTTP client with decorator-based routing, async support, and beautiful logging.

Tests Package version Supported Python versions CodSpeed GitHub Stars


Documentation: https://fasthttp.ndugram.dev/ru/latest/

Source Code: https://github.com/ndugram/fasthttp


FastHTTP is a modern async HTTP client library for Python, built on top of httpx. It brings a decorator-based API — similar to FastAPI, but for outgoing requests — with structured logging, middleware, Pydantic validation, and a built-in Swagger UI.

Key features:

  • Fast — built on httpx with full async support and parallel request execution.
  • Simple — define HTTP requests as decorated async functions, no boilerplate.
  • Typed — full type annotations throughout; validate responses with Pydantic models.
  • Logged — colorful, structured request/response logs with timing, built-in.
  • Complete — GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, and GraphQL out of the box.
  • Extensible — middleware, dependency injection, routers, lifespan hooks.
  • Interactive — built-in Swagger UI via app.web_run() to browse and execute requests in the browser.
  • HTTP/2 — optional HTTP/2 support, with automatic fallback to HTTP/1.1.

Requirements

Python 3.10+

FastHTTP depends on:

  • httpx — async HTTP transport.
  • pydantic — response model validation and serialization.
  • orjson — fast JSON parsing.
  • typer — CLI interface.
  • uvicorn — ASGI server for web_run().

Installation

$ pip install fasthttp-client

---> 100%

Example

Create it

Create a file main.py:

from fasthttp import FastHTTP
from fasthttp.response import Response

app = FastHTTP()


@app.get(url="https://httpbin.org/get")
async def get_data(resp: Response) -> dict:
    return resp.json()


if __name__ == "__main__":
    app.run()

Run it

$ python main.py

Check it

You will see output like:

16:09:18.955 │ INFO     │ fasthttp │ ✔ FastHTTP started
16:09:19.519 │ INFO     │ fasthttp │ ✔ GET https://httpbin.org/get [200] 458.26ms
16:09:20.037 │ INFO     │ fasthttp │ ✔ Done in 1.08s

The resp object gives you access to status, headers, and body. resp.json() returns the parsed response:

{
    "args": {},
    "headers": {
        "Accept": "*/*",
        "Host": "httpbin.org",
        "User-Agent": "python-httpx/0.28.1"
    },
    "origin": "...",
    "url": "https://httpbin.org/get"
}

Interactive API docs

Replace app.run() with app.web_run():

from fasthttp import FastHTTP
from fasthttp.response import Response

app = FastHTTP()


@app.get(url="https://jsonplaceholder.typicode.com/users/1")
async def get_user(resp: Response) -> dict:
    return resp.json()


@app.post(url="https://jsonplaceholder.typicode.com/users")
async def create_user(resp: Response) -> dict:
    return resp.json()


if __name__ == "__main__":
    app.web_run()

Now go to http://127.0.0.1:8000/docs.

You will see the automatic interactive API documentation:

Expand any route to inspect parameters, schemas, and expected responses:

Click Try it out to execute the request directly from the browser and see the real response:

Upgrade the example

Now modify main.py to get more out of FastHTTP. Each upgrade below builds on the previous one.

With Pydantic response models...

Declare a Pydantic model and pass it as response_model. FastHTTP will validate and parse the response automatically:

from fasthttp import FastHTTP
from fasthttp.response import Response
from pydantic import BaseModel


class User(BaseModel):
    id: int
    name: str
    email: str


app = FastHTTP()


@app.get(
    url="https://jsonplaceholder.typicode.com/users/1",
    response_model=User,
)
async def get_user(resp: Response) -> User:
    return User(**resp.json())


if __name__ == "__main__":
    app.run()
With multiple HTTP methods...

Register as many routes as you need across all HTTP methods. FastHTTP runs them concurrently:

from fasthttp import FastHTTP
from fasthttp.response import Response

app = FastHTTP()


@app.get(url="https://httpbin.org/get")
async def get_data(resp: Response) -> dict:
    return resp.json()


@app.post(url="https://httpbin.org/post")
async def post_data(resp: Response) -> dict:
    return resp.json()


@app.put(url="https://httpbin.org/put")
async def put_data(resp: Response) -> dict:
    return resp.json()


@app.patch(url="https://httpbin.org/patch")
async def patch_data(resp: Response) -> dict:
    return resp.json()


@app.delete(url="https://httpbin.org/delete")
async def delete_data(resp: Response) -> int:
    return resp.status_code


@app.head(url="https://httpbin.org/get")
async def head_data(resp: Response) -> int:
    return resp.status


@app.options(url="https://httpbin.org/get")
async def options_data(resp: Response) -> dict:
    return {"allow": resp.headers.get("allow", "")}


if __name__ == "__main__":
    app.run()
With routers...

Group related routes into a Router with a shared prefix or base URL, then include it into the app:

from fasthttp import FastHTTP, Router
from fasthttp.response import Response

users_router = Router(prefix="https://jsonplaceholder.typicode.com")


@users_router.get(url="/users/1")
async def get_user(resp: Response) -> dict:
    return resp.json()


@users_router.get(url="/users/2")
async def get_user_two(resp: Response) -> dict:
    return resp.json()


@users_router.post(url="/users")
async def create_user(resp: Response) -> dict:
    return resp.json()


app = FastHTTP()
app.include_router(users_router)

if __name__ == "__main__":
    app.run()
With middleware...

Intercept and modify requests before they are sent and responses after they are received:

from fasthttp import FastHTTP
from fasthttp.middleware import BaseMiddleware
from fasthttp.response import Response


class LoggingMiddleware(BaseMiddleware):
    __priority__ = 0
    __methods__ = None
    __enabled__ = True

    async def request(self, method: str, url: str, kwargs: dict) -> dict:
        print(f"→ {method} {url}")
        return kwargs

    async def response(self, response: Response) -> Response:
        print(f"← {response.status}")
        return response


app = FastHTTP(middleware=[LoggingMiddleware()])


@app.get(url="https://httpbin.org/get")
async def get_data(resp: Response) -> dict:
    return resp.json()


if __name__ == "__main__":
    app.run()
With dependency injection...

Use Depends to share logic across routes — auth tokens, computed headers, or any reusable setup:

from fasthttp import FastHTTP, Depends
from fasthttp.response import Response
from fasthttp.types import RequestsOptinal


def auth_headers() -> RequestsOptinal:
    return {"headers": {"Authorization": "Bearer my-token"}}


app = FastHTTP()


@app.get(
    url="https://httpbin.org/get",
    dependencies=[Depends(auth_headers)],
)
async def get_data(resp: Response) -> dict:
    return resp.json()


if __name__ == "__main__":
    app.run()
With lifespan...

Run setup and teardown logic around your requests using an async context manager:

from contextlib import asynccontextmanager

from fasthttp import FastHTTP
from fasthttp.response import Response


@asynccontextmanager
async def lifespan(app: FastHTTP):
    print("Startup: loading credentials...")
    app.token = "my-secret-token"  # type: ignore[attr-defined]
    yield
    print("Shutdown: cleanup done.")


app = FastHTTP(lifespan=lifespan)


@app.get(url="https://httpbin.org/get")
async def get_data(resp: Response) -> dict:
    return resp.json()


if __name__ == "__main__":
    app.run()
With GraphQL...

Use @app.graphql to send queries and mutations. The handler returns the query body; FastHTTP sends it and gives you the parsed response:

from fasthttp import FastHTTP
from fasthttp.response import Response


app = FastHTTP()


@app.graphql(url="https://countries.trevorblades.com/graphql")
async def get_countries(resp: Response) -> dict:
    return {
        "query": """
            {
                countries {
                    name
                    code
                    capital
                }
            }
        """
    }


if __name__ == "__main__":
    app.run()

Optional dependencies

$ pip install fasthttp-client[http2]

Enable HTTP/2 per app instance:

app = FastHTTP(http2=True)

Servers that don't support HTTP/2 fall back to HTTP/1.1 automatically.

CLI

FastHTTP ships with a command-line client. After installation, the fasthttp command is available globally.

Quick HTTP requests

$ fasthttp get https://httpbin.org/get json
$ fasthttp post https://httpbin.org/post json -j '{"name": "alice"}'
$ fasthttp delete https://httpbin.org/delete status

Output format is the last positional argument: status · headers · json · text · all

$ fasthttp get https://httpbin.org/get all
Status: 200
Elapsed: 312.45ms
Headers:
{ ... }
Body:
{ ... }

Pass headers, timeout, and proxy via options:

$ fasthttp get https://api.example.com/users json \
    -H "Authorization:Bearer token,Accept:application/json" \
    --timeout 10 \
    --proxy http://proxy.example.com:8080

Run your app from CLI

Execute all registered routes in a main.py without calling python main.py:

$ fasthttp run main.py

Start the dev server with Swagger UI:

$ fasthttp dev main.py
$ fasthttp dev main.py --host 0.0.0.0 --port 9000

GraphQL

$ fasthttp graphql https://countries.trevorblades.com/graphql \
    -q "{ countries { name code } }" \
    json

Interactive REPL

$ fasthttp repl

Or just fasthttp with no arguments — drops you into the interactive shell.

Command reference

Command Description
fasthttp get <url> [output] GET request
fasthttp post <url> [output] POST request
fasthttp put <url> [output] PUT request
fasthttp patch <url> [output] PATCH request
fasthttp delete <url> [output] DELETE request
fasthttp graphql <url> -q <query> GraphQL query or mutation
fasthttp run <file.py> Run all routes from a file
fasthttp dev <file.py> Start dev server with Swagger UI
fasthttp repl Interactive REPL
fasthttp version Show version

Contributing

Contributions are welcome! Please read the Contributing Guide before opening a pull request.

Found a security issue? See the Security Policy.

License

This project is licensed under the terms of the MIT license.