January 30, 2026
dequest api call declarative client

Make Python API Calls a Breeze with dequest Declarative Client

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:

Leave a Reply

Your email address will not be published. Required fields are marked *