Gavin Wiggins


Quart App with HTTPX Client and Global Config

Published on February 1, 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 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 or from the shell environment using the load_dotenv function. A global configuration object isn't really needed in this example since everything is obtained from environment variables. But it could be useful for larger projects.

# config.py

import os
from dataclasses import dataclass
from dotenv import load_dotenv

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

    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()
        base_url = os.environ["BASE_URL"]
        timeout = os.environ["TIMEOUT"]
        _config = Config(base_url=base_url, timeout=int(timeout))

    return _config

Tests

The pytest-env plugin enables the environment variables for the tests by defining them in the pyproject.toml file as shown below. This is especially useful when secret keys or passwords are obtained from the environment but you don't want to use the real key for the tests.

[tool.pytest_env]
BASE_URL = "https://example.com"
TIMEOUT = 40

Next are the tests for the config module. Notice the environment variables have already been set and are available to the config object.

# test_config.py

import pytest
from myapp import config
from myapp.config import get_config

@pytest.fixture(autouse=True)
def reset_config():
    """Reset global config object for each test."""
    config._config = None

def test_config():
    """Test config settings."""
    conf = get_config()
    assert conf.base_url == "https://example.com"
    assert conf.timeout == 40

def test_config_global():
    """Test global config object for equality."""
    config1 = get_config()
    config2 = get_config()

    assert config1 is config2
    assert config1 == config2

def test_missing_env(monkeypatch):
    """Test for error when environment variables are missing."""
    monkeypatch.delenv("BASE_URL", raising=False)
    monkeypatch.delenv("TIMEOUT", raising=False)

    with pytest.raises(KeyError):
        get_config()

Tests for the HTTP 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 myapp.http_client as http_module
from myapp.http_client import init_client, get_client, close_client

@pytest.fixture(autouse=True)
def reset_client():
    """Reset global HTTP client for each test."""
    http_module._client = None

def test_missing_client():
    """Test for error when client has not been initialized."""
    with pytest.raises(RuntimeError, match="Client not initialized"):
        get_client()

def test_init_client():
    """Test client initialization."""
    init_client()
    client = get_client()

    assert client is not None
    assert client.timeout.connect == 40

@pytest.mark.asyncio
async def test_close_client():
    """Test for error after closing the client."""
    init_client()

    await close_client()

    with pytest.raises(RuntimeError, match="Client not initialized"):
        get_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 myapp.main import app
from myapp import config
from myapp.http_client import init_client, close_client

@pytest_asyncio.fixture
async def client():
    """Initialize the HTTP client and yield an app client."""
    config._config = None
    init_client()
    test_client = app.test_client()
    yield test_client
    await close_client()

@pytest.mark.asyncio
async def test_root(client):
    """Test the root endpoint."""
    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_users(client):
    """Test the users endpoint."""
    mock_users = [
        {"id": 1, "name": "John Doe", "email": "john@example.com"},
        {"id": 2, "name": "Jane Doe", "email": "jane@example.com"},
    ]

    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_posts(client):
    """Test the posts endpoint."""
    mock_posts = [
        {"id": 1, "title": "First Post", "body": "Hello world", "userId": 1},
        {"id": 2, "title": "Second Post", "body": "Another post", "userId": 1},
    ]

    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, RESPX, and pytest-env documentation for more information about the pytest plugins. Code discussed in this article is available in the pythonic/projects/quart-httpx-config directory which is located in the GitHub pythonic repository.


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