Build an API Client
So far, you learned HTTP, Requests, JSON, REST APIs, authentication, and error handling. Now we will combine everything into a reusable Python API client.
An API client is Python code that knows how to communicate with an API.
Instead of writing the same request code repeatedly, we create reusable functions or a class that handles authentication, requests, errors, and JSON responses.
What Is an API Client?
An API client is code that communicates with an API on behalf of your application.
Without a client, you might repeatedly write code like:
import requests
response = requests.get(
"https://api.example.com/products"
)
data = response.json()
print(data)
If your application needs many API endpoints, repeating this code becomes messy.
Instead, we create a reusable API client.
Your Application
↓
API Client
↓
HTTP Request
↓
API
↓
JSON Response
↓
API Client
↓
Your Application
Why Build an API Client?
A reusable client gives you one place to manage API communication.
- Authentication
- Base URL
- HTTP requests
- Timeouts
- Error handling
- JSON responses
- Reusable API methods
This becomes especially useful when your application communicates with multiple endpoints.
Use a Base URL
Most APIs have a common base URL.
https://api.example.com
Individual endpoints are added to that base URL.
/products /users /orders
Instead of repeating the full domain everywhere, store it once.
BASE_URL = "https://api.example.com"
Build a Simple API Client
Start with a simple function.
import requests
BASE_URL = "https://api.example.com"
def get_products():
response = requests.get(
f"{BASE_URL}/products"
)
response.raise_for_status()
return response.json()
products = get_products()
print(products)
The function hides the HTTP details from the rest of your application.
Sending Parameters
APIs often accept query parameters.
For example:
/products?limit=10
With Requests, use the params argument.
import requests
BASE_URL = "https://api.example.com"
def get_products(limit=10):
params = {
"limit": limit
}
response = requests.get(
f"{BASE_URL}/products",
params=params
)
response.raise_for_status()
return response.json()
products = get_products(10)
print(products)
Requests creates the query string for you.
Add Authentication
Our client can now include an API key.
import os
import requests
from dotenv import load_dotenv
load_dotenv()
BASE_URL = "https://api.example.com"
API_KEY = os.getenv("API_KEY")
headers = {
"X-API-Key": API_KEY
}
The key should come from an environment variable, not from hardcoded source code.
Create a Reusable Request Method
Instead of repeating authentication and error handling in every API method, create one function responsible for making requests.
import os
import requests
from dotenv import load_dotenv
load_dotenv()
class APIClient:
def __init__(self, base_url):
self.base_url = base_url
self.api_key = os.getenv("API_KEY")
self.headers = {
"X-API-Key": self.api_key
}
def request(self, method, endpoint, **kwargs):
url = f"{self.base_url}{endpoint}"
response = requests.request(
method,
url,
headers=self.headers,
timeout=10,
**kwargs
)
response.raise_for_status()
return response.json()
Now authentication and common request behavior are handled in one place.
Add a GET Method
We can now create a clean method for retrieving products.
class APIClient:
def __init__(self, base_url):
self.base_url = base_url
self.api_key = os.getenv("API_KEY")
self.headers = {
"X-API-Key": self.api_key
}
def request(self, method, endpoint, **kwargs):
url = f"{self.base_url}{endpoint}"
response = requests.request(
method,
url,
headers=self.headers,
timeout=10,
**kwargs
)
response.raise_for_status()
return response.json()
def get_products(self):
return self.request(
"GET",
"/products"
)
The application now only needs to call:
client = APIClient(
"https://api.example.com"
)
products = client.get_products()
print(products)
Add a POST Method
APIs also allow applications to create resources.
For example, creating a product might require a POST request.
def create_product(self, product):
return self.request(
"POST",
"/products",
json=product
)
Then:
product = {
"name": "AI Laptop",
"price": 1200
}
created_product = client.create_product(
product
)
print(created_product)
Notice that the application does not need to know how the HTTP request is constructed.
PUT and DELETE
The same client can support other HTTP methods.
Update a Product
def update_product(self, product_id, product):
return self.request(
"PUT",
f"/products/{product_id}",
json=product
)
Delete a Product
def delete_product(self, product_id):
return self.request(
"DELETE",
f"/products/{product_id}"
)
Add Error Handling
A real client must handle network and HTTP errors.
def request(self, method, endpoint, **kwargs):
url = f"{self.base_url}{endpoint}"
try:
response = requests.request(
method,
url,
headers=self.headers,
timeout=10,
**kwargs
)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
print("The API request timed out.")
except requests.exceptions.ConnectionError:
print("Could not connect to the API.")
except requests.exceptions.HTTPError as error:
print("API returned an HTTP error:", error)
except requests.exceptions.JSONDecodeError:
print("The API returned invalid JSON.")
return None
Complete API Client
Now put everything together.
import os
import requests
from dotenv import load_dotenv
load_dotenv()
class APIClient:
def __init__(self, base_url):
self.base_url = base_url
self.api_key = os.getenv("API_KEY")
if not self.api_key:
raise ValueError(
"API_KEY is not configured"
)
self.headers = {
"X-API-Key": self.api_key
}
def request(self, method, endpoint, **kwargs):
url = f"{self.base_url}{endpoint}"
try:
response = requests.request(
method,
url,
headers=self.headers,
timeout=10,
**kwargs
)
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
print("The API request timed out.")
except requests.exceptions.ConnectionError:
print(
"Could not connect to the API."
)
except requests.exceptions.HTTPError as error:
print(
"API returned an HTTP error:",
error
)
except requests.exceptions.JSONDecodeError:
print(
"The API returned invalid JSON."
)
return None
def get_products(self):
return self.request(
"GET",
"/products"
)
def get_product(self, product_id):
return self.request(
"GET",
f"/products/{product_id}"
)
def create_product(self, product):
return self.request(
"POST",
"/products",
json=product
)
def update_product(self, product_id, product):
return self.request(
"PUT",
f"/products/{product_id}",
json=product
)
def delete_product(self, product_id):
return self.request(
"DELETE",
f"/products/{product_id}"
)
Using the API Client
The application can now create the client and call simple methods.
client = APIClient(
"https://api.example.com"
)
products = client.get_products()
print(products)
Get one product:
product = client.get_product(10) print(product)
Create a product:
new_product = {
"name": "AI Laptop",
"price": 1200
}
product = client.create_product(
new_product
)
print(product)
Organize the Project
Once the client becomes larger, separate it from the main application.
api-client-project/ │ ├── .env ├── .gitignore ├── requirements.txt │ ├── api_client.py │ └── main.py
The API communication belongs in
api_client.py.
The application logic belongs in
main.py.
How the API Client Works
main.py ↓ APIClient ↓ Authentication ↓ Build URL ↓ HTTP Request ↓ API Server ↓ HTTP Response ↓ Check Errors ↓ Parse JSON ↓ Return Data ↓ main.py
The application does not need to worry about every HTTP detail. The client handles that complexity.
Why This Matters for AI
This pattern becomes extremely useful when building AI applications.
Later, you may communicate with services for:
- LLM generation
- Embeddings
- Vector databases
- Document processing
- Image generation
- Speech processing
- External business APIs
Your AI Application
↓
Python Client
↓
Authentication
↓
HTTP Request
↓
AI API
↓
JSON Response
↓
AI Application
Understanding API clients now will make later LLM and RAG lessons much easier.
Mini Project — Product API Client
Build a complete client for a fictional product API.
Step 1 — Environment
API_KEY=your_api_key
Step 2 — Client
from api_client import APIClient
client = APIClient(
"https://api.example.com"
)
products = client.get_products()
if products is not None:
for product in products:
print(product)
Step 3 — Test Another Endpoint
product = client.get_product(1) print(product)
The goal is not simply to make one API request. The goal is to create reusable code that can communicate with multiple endpoints.
Good API Client Practices
- Keep API credentials outside source code.
- Use environment variables for secrets.
- Set request timeouts.
- Handle HTTP errors.
- Handle connection failures.
- Keep API communication in one place.
- Use reusable methods.
- Keep application logic separate from API logic.
- Do not blindly assume every response is valid JSON.
- Read the API documentation before implementing authentication or endpoints.
What You Learned
- What an API client is
- Why reusable API clients are useful
- Using a base URL
- Creating API client functions
- Using GET requests
- Using POST requests
- Using PUT requests
- Using DELETE requests
- Adding authentication
- Handling API errors
- Handling network failures
- Parsing JSON responses
- Creating a reusable API client class
- Organizing an API client project
- Why API clients matter for AI applications
Stop repeating API code. Build reusable clients.
A good API client hides authentication, URLs, HTTP requests, error handling, and response parsing behind simple methods. Your application then focuses on what to do with the data instead of repeatedly managing HTTP details.