Gavin Wiggins


Quart Application with HTTPX Client

Published on January 17, 2026

The Quart app shown below demonstrates the use of an HTTPX AsyncClient within the /users and /posts routes. The purpose of the client is to make JSON requests to some service and the returned data can be used within the Quart application. The client is initialized during startup and closed during shutdown.

# main.py

from quart import Quart
from .http_client import init_client, get_client, close_client


app = Quart(__name__)


@app.before_serving
async def startup():
    """Create a global HTTP async client before serving."""
    init_client()


@app.after_serving
async def shutdown():
    """Clean up resources after serving."""
    await close_client()


@app.get("/")
async def root():
    """Return server status."""
    return {"status": "ok", "message": "Server is running"}


@app.get("/users")
async def get_users():
    """Fetch and return user data from JSONPlaceholder API."""
    client = get_client()
    response = await client.get("https://jsonplaceholder.typicode.com/users")
    return response.json()


@app.get("/posts")
async def get_posts():
    """Fetch and return posts data from JSONPlaceholder API."""
    client = get_client()
    response = await client.get("https://jsonplaceholder.typicode.com/posts")
    return response.json()

The module where the client is defined is shown below. Only one HTTP client is needed throughout the app but it is not initialized at the module level because this would cause unwanted side effects during package import (like in testing). Therefore, it is initialized and configured within in the init_client function as a global variable. The client object is returned from the get_client function and closed with the close_client function. This approach ensures that only one client object is created and used throughout the app.

# http_client.py

import httpx
from .config import get_config


_client: httpx.AsyncClient | None = None


def init_client() -> None:
    """Initialize a global HTTP async client."""
    global _client

    conf = get_config()
    _client = httpx.AsyncClient(base_url=conf.base_url, timeout=conf.timeout)


def get_client() -> httpx.AsyncClient:
    """Get the HTTP async client or throw error."""
    if _client is None:
        raise RuntimeError("Client not initialized")
    return _client


async def close_client() -> None:
    """Close the HTTP async client."""
    global _client

    if _client:
        await _client.aclose()
        _client = None

The configuration module is shown next. A dataclass is used to store the values because it enables code completion via dot syntax. It is frozen to make it immutable and slots are enabled to reduce memory. Similar to the approach used for the client object, the get_config function returns the global config object. Environment variables are loaded from a .env file using the load_dotenv function.

# config.py

import os
from dataclasses import dataclass
from dotenv import load_dotenv


@dataclass(frozen=True, slots=True)
class Config:
    """Configuration settings."""

    base_url: str
    timeout: int


_config: Config | None = None


def get_config() -> Config:
    """Get configuration settings (created once on first call)."""
    global _config

    if _config is None:
        load_dotenv()
        _config = Config(
            base_url=os.environ["BASE_URL"], timeout=int(os.environ["TIMEOUT"])
        )

    return _config

For testing, some shared fixtures are defined as shown below. The fixtures reset the global config object and manipulate the environment variables.

# conftest.py

import pytest
from quart_httpx import config


@pytest.fixture(autouse=True)
def reset_config(monkeypatch):
    """Reset global config and prevent load_dotenv from loading .env file."""
    config._config = None
    monkeypatch.setattr("quart_httpx.config.load_dotenv", lambda: None)


@pytest.fixture
def mock_env_vars(monkeypatch):
    """Mock environment variables."""
    monkeypatch.setenv("BASE_URL", "https://example.com")
    monkeypatch.setenv("TIMEOUT", "40")


@pytest.fixture
def mock_env_missing(monkeypatch):
    """Mock missing environment variables."""
    monkeypatch.delenv("BASE_URL", raising=False)
    monkeypatch.delenv("TIMEOUT", raising=False)

Next are the tests for the config module. Notice the environment variables are handled by the fixtures.

# test_config.py

import pytest
from quart_httpx.config import get_config


def test_get_config(mock_env_vars):
    conf = get_config()
    assert conf.base_url == "https://example.com"
    assert conf.timeout == 40


def test_get_config_is_cached(mock_env_vars):
    config1 = get_config()
    config2 = get_config()

    assert config1 is config2


def test_get_config_missing(mock_env_missing):
    with pytest.raises(KeyError):
        get_config()

Tests for the client module are shown below. The pytest-asyncio plugin provides async functionality to pytest via the @pytest.mark.asyncio function decorator.

# test_http_client.py

import pytest
import quart_httpx.http_client as http_module
from quart_httpx.http_client import init_client, get_client, close_client


@pytest.fixture(autouse=True)
def reset_client():
    """Reset the HTTP client state before and after each test."""
    http_module._client = None
    yield
    http_module._client = None


def test_get_client_raises_when_not_initialized():
    with pytest.raises(RuntimeError, match="Client not initialized"):
        get_client()


def test_init_client(mock_env_vars):
    init_client()

    client = get_client()
    assert client is not None
    assert client.timeout.connect == 40


@pytest.mark.asyncio
async def test_close_client(mock_env_vars):
    init_client()

    await close_client()

    with pytest.raises(RuntimeError, match="Client not initialized"):
        get_client()


@pytest.mark.asyncio
async def test_close_client_when_none():
    await close_client()

Lastly, the tests for the main module are given below. The respx plugin allows us to mock the response from the HTTPX client by using the @respx.mock decorator.

# test_main.py

import pytest
import pytest_asyncio
import respx
from httpx import Response

from quart_httpx.main import app
from quart_httpx import config
from quart_httpx.http_client import init_client, close_client


MOCK_USERS = [
    {"id": 1, "name": "John Doe", "email": "john@example.com"},
    {"id": 2, "name": "Jane Doe", "email": "jane@example.com"},
]

MOCK_POSTS = [
    {"id": 1, "title": "First Post", "body": "Hello world", "userId": 1},
    {"id": 2, "title": "Second Post", "body": "Another post", "userId": 1},
]


@pytest_asyncio.fixture
async def client(mock_env_vars):
    config._config = None
    init_client()
    test_client = app.test_client()
    yield test_client
    await close_client()


@pytest.mark.asyncio
async def test_root(client):
    response = await client.get("/")
    assert response.status_code == 200

    data = await response.get_json()
    assert data == {"status": "ok", "message": "Server is running"}


@pytest.mark.asyncio
@respx.mock
async def test_get_users(client):
    respx.get("https://jsonplaceholder.typicode.com/users").mock(
        return_value=Response(200, json=MOCK_USERS)
    )

    response = await client.get("/users")
    assert response.status_code == 200

    data = await response.get_json()
    assert isinstance(data, list)
    assert len(data) == 2
    assert data[0]["id"] == 1
    assert data[0]["name"] == "John Doe"
    assert data[0]["email"] == "john@example.com"


@pytest.mark.asyncio
@respx.mock
async def test_get_posts(client):
    respx.get("https://jsonplaceholder.typicode.com/posts").mock(
        return_value=Response(200, json=MOCK_POSTS)
    )

    response = await client.get("/posts")
    assert response.status_code == 200

    data = await response.get_json()
    assert isinstance(data, list)
    assert len(data) == 2
    assert data[0]["id"] == 1
    assert data[0]["title"] == "First Post"
    assert data[0]["body"] == "Hello world"

Further reading

See the Quart and HTTPX documentation for more information about these packages. See the pytest-asyncio and RESPX documentation for more information about the pytest plugins. Code discussed in this article is available in the pythonic/projects/quart-httpx directory which is located in the GitHub pythonic repository.


Gavin Wiggins © 2026
Made on a Mac with Genja. Hosted on GitHub Pages.