Making HTTP requests in Python often involves repetitive code for handling parameters, authentication, retries, and error handling. In modern applications, making HTTP requests reliably is critical—especially when dealing with microservices, third-party APIs, or distributed systems. What if we could encapsulate all of this in a declarative way while maintaining type safety and flexibility? dequest is here to save the day! With its @sync_client and @async_client decorators, you can make API calls declarative, elegant, and supercharged with caching, retries, and circuit breakers—without dealing with requests or asyncio packages manually.
In this post, I’ll show you how to:
- Use sync / async API call easily
- Fetch data with zero boilerplate
- Handle path, query, and form parameters
- Use retries, circuit breakers, and caching for resilient async APIs
- Work with callbacks and fallbacks
Let’s get started!
1. Install dequest and Set Up
First, install the package:
pip install dequest
That’s it! Now you’re ready to make API calls like a boss.
2. Fetching Data with sync_client
Imagine you need to fetch a list of users by city. Normally, you’d write a bunch of requests.get() calls, handle parameters, check for errors, and parse JSON. But with dequest, it’s as simple as defining a function:
from dequest import sync_client, QueryParameter
from typing import List
from dataclasses import dataclass
@dataclass
class UserDto:
id: int
name: str
city: str
@sync_client(url="https://jsonplaceholder.typicode.com/users", dto_class=UserDto)
def get_users(city: QueryParameter[str, "city_name"]) -> List[UserDto]:
pass # No implementation needed!
users = get_users(city="New York")
print(users)
Boom! 🎇 This sends a GET request to:
https://jsonplaceholder.typicode.com/users?city_name=New%20York
…and automatically maps the response to UserDto objects.
✅ No manual request handling
✅ No JSON parsing
✅ No error handling needed (it’s built-in!)
3. Using Path Parameters
Need to fetch a single user by ID? Use PathParameter:
from dequest import sync_client, PathParameter
@sync_client(url="https://jsonplaceholder.typicode.com/users/{user_id}", dto_class=UserDto)
def get_user(user_id: PathParameter[int]) -> UserDto:
pass
user = get_user(user_id=1)
print(user)
This generates:
GET https://jsonplaceholder.typicode.com/users/1
Nice and clean, right?
4. Sending Data in a POST Request
If you need to create a new user, just use FormParameter:
from dequest import sync_client, FormParameter
from dequest.enums import HttpMethod
@sync_client(url="https://some-url-to-save-users/users", method=HttpMethod.POST, dto_class=UserDto)
def create_user(name: FormParameter[str], city: FormParameter[str]) -> UserDto:
pass
new_user = create_user(name="Alice", city="Berlin")
print(new_user)
This will automatically send form data in a POST request.
Want to send JSON instead? Use JsonBody:
from dequest import sync_client, JsonBody
@sync_client(url="https://jsonplaceholder.typicode.com/users", method="POST", dto_class=UserDto)
def create_user(name: JsonBody, city:JsonBody) -> UserDto:
pass
new_user = create_user(name="Alice", "city"="Berlin")
print(new_user)
The above function call, send this request to the url:
curl -X POST https://some-url-to-save-users.com/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "city": "Berlin"}'
5. Retrying Failed Requests
Ever had an API randomly fail? Instead of manually retrying, just tell dequest to automatically retry failed requests:
@sync_client(url="https://api.example.com/data", retries=3, retry_delay=2.0,retry_on_exceptions=(HTTPError,))
def get_data():
pass
🔁 This will retry up to 3 times, with a 2-second delay between attempts.
Works for any HTTPError—no extra code needed! If you need more control on retry we he giveup for specifying when and in which condition should the client raise error instead of retry:
from dequest import async_client, HttpMethod
from requests import HTTPError
import http
@async_client(
url="https://dummyjson.com/auth/me",
method=HttpMethod.GET,
dto_class=UserDTO,
retries=3,
retry_delay=1,
retry_on_exceptions=(HTTPError,),
# Retry only if the error is an internal server error (500)
giveup=lambda e: e.response.status_code != http.HTTPStatus.INTERNAL_SERVER_ERROR,
)
def get_current_user() -> UserDTO:
"""
Retrieve the current user's information from the API.
"""
6. Caching API Responses
Why hit the server every time if the data doesn’t change? Enable caching:
@sync_client(url="https://api.example.com/popular-posts", enable_cache=True, cache_ttl=60)
def get_popular_posts():
pass
This caches the response for 60 seconds—saving API calls and speeding up the processes. It generates a cache_key by combining the URL, including the values of path parameters, with query parameters, ensuring that each API request is cached separately.
7. Using a Circuit Breaker for Resilience
Some APIs go down frequently. Instead of hammering a broken server, use a circuit breaker:
from dequest import sync_client
from dequest.circuit_breaker import CircuitBreaker
breaker = CircuitBreaker(failure_threshold=5, recovery_timeout=30)
@sync_client(url="https://api.unstable.com/data", circuit_breaker=breaker)
def get_unstable_data():
pass
If the API fails 5 times, the circuit opens (blocks requests for 30 sec), after 30 sec, it resumes requests and checks if the API is back. This prevents overloading a failing service!
If a CircuitBreaker instance is provided and disallows a request, the fallback function is invoked (if defined); otherwise, a CircuitBreakerOpenError is raised:
from dequest import sync_client
from dequest.circuit_breaker import CircuitBreaker, CircuitBreakerOpenError
# Define a fallback function that returns cached data
def fallback_response():
return {"message": "Service unavailable, returning cached data instead"}
# Create a Circuit Breaker instance with a fallback
breaker = CircuitBreaker(
failure_threshold=3,
recovery_timeout=10,
fallback_function=fallback_response # Set fallback at the CircuitBreaker level
)
@sync_client(
url="https://api.unstable.com/data",
circuit_breaker=breaker
)
def fetch_unstable_data():
pass
# Simulating multiple failed requests
try:
for _ in range(5): # This will exceed the failure threshold
response = fetch_unstable_data()
print(response)
except CircuitBreakerOpenError: # it won't raise
print("Circuit breaker is open! Too many failures, try again later.")
Note : The retry logic wraps the entire operation so that only if all attempts fail does the circuit breaker record one failure.
8. Making Asynchronous API Calls with async_client
Many times we just need hit an API and the result is not important for us, howeverm if we call the API with sync_client the system should wait for the call be finished to let the fellow go on. In such scenarios we can use @async_client decorator to make the API async without pain.
@async_client(url="https://api.example.com/notify", method=HttpMethod.POST)
def notify():
pass
notify()
A great feature of @async_client is that you don’t need to use it exclusively in an async function or execute it with asyncio. As shown in the example, you can simply call the function, and it handles everything for you. However, keep in mind that it won’t wait for the result and will immediately proceed to the next line of code. If you want to automatically process responses whenever it got ready? Just pass a callback:
async def process_data(data):
print(f"Received data: {data}")
@async_client(url="https://api.example.com/updates", callback=process_data)
def get_updates():
pass
Every time get_updates() is called, process_data() will run automatically with the response.
9. Configuring dequest: Global Settings
Dequest allows global configuration via DequestConfig, the configuration can be set using .config method of the DequestConfig class:
from dequest import DequestConfig
DequestConfig.config(
cache_provider="redis", # defaults to "in_memory"
redis_host="my-redis-server.com",
redis_port=6380,
redis_db=1,
redis_password="securepassword",
redis_ssl=True,
)
Using the .config() method is optional—you can call it on app startup if you want to override the default settings with custom configurations.
Final Words
Dequest makes API calls in Python clean, reliable, and fast. With decorators like @sync_client and @async_client, you get built-in support for retries, caching, circuit breakers, and more without the boilerplate. It’s production-ready and developer-friendly. Give it a try and simplify your service calls!
Happy coding! 🚀
References: