The Python Requests module simplifies making HTTP requests in Python. In this guide, we’ll explore everything from basic usage to advanced features, ensuring you have the knowledge to effectively utilize this powerful library in your projects. We’ll cover making various types of HTTP requests, handling responses, managing errors, and implementing best practices for production applications.
Getting Started with Requests
Installing Requests is straightforward. You can install it using either pip or conda:
# Using pip
pip install requests
# Using conda
conda install requests
Basic Setup
After installation, import the library in your Python script:
import requests
To verify everything is working, try a simple request:
response = requests.get('https://api.github.com')
print(response.status_code) # Should print 200 if successful
This sends a GET request to GitHub’s API. The status code 200
indicates a successful response, confirming that both your internet connection and the Requests library are working properly.
Basic HTTP Requests
HTTP defines several request methods (or “verbs”) that indicate the desired action to be performed on a resource. Let’s explore each type and when to use them:
Making GET Requests
GET requests retrieve data from a specified resource. They should be used when you want to fetch information without modifying server data (idempotent operation).
import requests
# Simple GET request
response = requests.get('https://dummyjson.com/products/1')
# GET request with query parameters
params = {'limit': 5, 'skip': 10}
response = requests.get('https://dummyjson.com/products', params=params)
In the first example, we make a simple GET request to retrieve data for a specific product. The second example demonstrates how to include query parameters, which will be automatically added to the URL (resulting in something like https://dummyjson.com/products?limit=5&skip=10
). This is useful for filtering, pagination, or providing additional context to your request.
When to use GET:
- Retrieving data from APIs
- Loading web pages
- Downloading resources
- Searches and queries
- Any read-only operation
POST Requests
POST requests submit data to be processed by a specified resource. They’re used when you need to create or add new resources to a server.
import requests
# Sending form data
form_data = {'username': 'john_doe', 'password': 'secret'}
response = requests.post('https://dummyjson.com/auth/login', data=form_data)
# Sending JSON data
json_data = {
"title": "New Product",
"description": "This is a new product",
"price": 249.99,
"brand": "MyPhone",
"category": "smartphones"
}
response = requests.post('https://dummyjson.com/products/add', json=json_data)
In the first example, we’re sending form-encoded data (like submitting an HTML form), which sets the Content-Type
header to application/x-www-form-urlencoded
. The second example sends JSON data (setting the Content-Type
to application/json
), which is the standard format for most modern APIs.
When to use POST:
- Creating new resources (users, articles, comments)
- Submitting form data
- Uploading files
- Operations that change server state and aren’t idempotent
- When data is too large to fit in a URL (GET requests have URL length limitations)
Other HTTP Methods
Requests supports all standard HTTP methods, each designed for specific use cases:
import requests
# PUT request - replaces an existing resource entirely
put_data = {
"title": "iPhone Galaxy +1",
"description": "An updated smartphone description",
"price": 1299.99,
"brand": "Apple",
"category": "smartphones"
}
requests.put('https://dummyjson.com/products/1', json=put_data)
# DELETE request - removes a specified resource
requests.delete('https://dummyjson.com/products/1')
# HEAD request - retrieves headers without body content
head_response = requests.head('https://dummyjson.com/products/1')
print(head_response.headers) # Shows headers but head_response.text will be empty
# PATCH request - applies partial modifications to a resource
patch_data = {"title": "Updated iPhone Title"}
requests.patch('https://dummyjson.com/products/1', json=patch_data)
PUT vs. POST vs. PATCH: Key Differences
- PUT: Creates a new resource or replaces an existing one entirely. It’s idempotent, meaning sending the same request multiple times has the same effect as sending it once.
- POST: Creates a new resource. It’s not idempotent—sending the same request twice typically creates two resources.
- PATCH: Updates part of an existing resource without affecting the rest. Useful for resource modifications when you don’t want to send the complete object.
When to use HEAD:
- Checking if a resource exists
- Getting metadata (size, type, modification date) without downloading the full content
- Testing links without retrieving the full page
When to use DELETE:
- Removing resources (users, posts, records)
- Canceling operations
Working with Request Components
Headers
Custom headers are essential for many HTTP operations:
import requests
headers = {
'User-Agent': 'MyApp/1.0 (contact@example.com)', # Identifies your application
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', # Provides authentication credentials
'Content-Type': 'application/json', # Specifies the format of the request body
'Accept': 'application/json' # Indicates preferred response format
}
response = requests.get('https://dummyjson.com/auth/products', headers=headers)
Headers provide metadata about the request. The User-Agent identifies your application to the server, Authorization carries credentials, Content-Type tells the server what format your data is in, and Accept tells the server what format you want the response in.
Parameters
Parameters can be passed in multiple ways:
import requests
# Query parameters - these become part of the URL after the question mark
params = {'q': 'phone', 'limit': 5}
response = requests.get('https://dummyjson.com/products/search', params=params)
print(response.url) # Will show https://dummyjson.com/products/search?q=phone&limit=5
# URL parameters - these are part of the URL path structure
product_id = 1
response = requests.get(f'https://dummyjson.com/products/{product_id}')
Query parameters are excellent for filtering, sorting, and pagination, while URL parameters are typically used to identify specific resources in RESTful APIs.
Request Body
Different types of request bodies:
import requests
# Form data - simulates an HTML form submission
form_data = {'username': 'kminchelle', 'password': '0lelplR'}
response = requests.post('https://dummyjson.com/auth/login', data=form_data)
# Content-Type will be set to application/x-www-form-urlencoded
# JSON data - the standard for most modern APIs
json_data = {
"title": "New Product",
"description": "This is a new product",
"price": 249.99,
"brand": "Apple",
"category": "smartphones"
}
response = requests.post('https://dummyjson.com/products/add', json=json_data)
# Content-Type will be set to application/json automatically
# Multipart form data (file upload)
files = {'file': ('document.pdf', open('document.pdf', 'rb'), 'application/pdf')}
data = {'description': 'My document'}
response = requests.post('https://dummyjson.com/upload', files=files, data=data)
# Content-Type will be set to multipart/form-data
The data
parameter is for form-encoded data, while json
is for JSON data. Using json
automatically sets the Content-Type header and serializes your Python dictionary into a JSON string. For file uploads, the files
parameter creates a multipart/form-data request.
Handling Responses
Response Properties
import requests
response = requests.get('https://dummyjson.com/products/1')
# Basic properties
print(response.status_code) # HTTP status code (e.g., 200, 404)
print(response.text) # Response content as string
print(response.headers) # Response headers as dictionary-like object
print(response.encoding) # Response encoding (e.g., 'utf-8')
print(response.url) # Final URL (after redirects)
print(response.history) # List of response objects from redirects
# Content-specific properties
json_response = response.json() # Parse JSON response into Python dictionary
binary_content = response.content # Get raw bytes (useful for images, PDFs, etc.)
The .status_code
lets you know if the request was successful (200-299), redirected (300-399), had client errors (400-499) or server errors (500-599). The .text
property gives you the decoded response body as a string, while .content
provides raw bytes.
Working with JSON Responses
import requests
response = requests.get('https://dummyjson.com/products/1')
if response.status_code == 200:
try:
data = response.json()
# Access JSON data like a Python dictionary
print(f"Product ID: {data['id']}")
print(f"Product Name: {data['title']}")
print(f"Price: ${data['price']}")
# Handle nested values
if 'images' in data:
print(f"First image URL: {data['images'][0]}")
# Iterate through arrays
if 'images' in data:
print("Product images:")
for i, image_url in enumerate(data['images']):
print(f"- Image {i+1}: {image_url}")
except ValueError:
print("Response was not valid JSON")
The .json()
method conveniently parses the JSON response into Python data structures (dictionaries and lists). It raises a ValueError
if the response isn’t valid JSON, which is why we wrap it in a try/except block.
Handling Text Responses
import requests
response = requests.get('https://dummyjson.com')
if response.status_code == 200:
# response.encoding can be automatically detected, but you can override it if needed
if response.encoding is None:
response.encoding = 'utf-8'
text_content = response.text
# Process text content
if '<html' in text_content.lower():
print("This is an HTML page")
# Find specific content
import re
title_match = re.search('<title>(.*?)</title>', text_content, re.IGNORECASE)
if title_match:
print(f"Page title: {title_match.group(1)}")
The .text
property gives you the response body decoded to a string using the encoding specified in the response headers (or one that’s automatically detected). You can override the encoding if needed.
Dealing with Binary Content
import requests
# Downloading an image
image_url = "https://dummyjson.com/image/200" # Random image generator with width=200px
response = requests.get(image_url)
if response.status_code == 200:
# Check if the content type is an image
if 'image' in response.headers.get('Content-Type', ''):
with open('downloaded_image.jpg', 'wb') as f:
f.write(response.content)
print("Image downloaded successfully")
else:
print("URL did not return an image")
# Downloading a large file with streaming
with requests.get('https://dummyjson.com/products?limit=100', stream=True) as response:
if response.status_code == 200:
with open('products.json', 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print("File downloaded successfully")
The .content
property gives you the raw bytes of the response, which is perfect for binary data. For large files, using stream=True
and iter_content()
is more memory-efficient as it downloads the file in chunks rather than loading everything into memory at once.
Error Handling and Exceptions
Common Exceptions
import requests
try:
response = requests.get('https://dummyjson.com/products/999', timeout=5)
# This will raise an exception for 4XX/5XX status codes
response.raise_for_status()
# Process the response (only executes if no exception was raised)
data = response.json()
print(f"Received data: {data}")
except requests.exceptions.HTTPError as e:
# Triggered by response.raise_for_status() for bad HTTP status codes
print(f"HTTP error occurred: {e}")
print(f"Response status code: {e.response.status_code}")
print(f"Response body: {e.response.text}")
except requests.exceptions.ConnectionError as e:
# Triggered when unable to connect to the host
print(f"Connection error occurred: {e}")
print("Please check your internet connection or if the server is available")
except requests.exceptions.Timeout as e:
# Triggered when the request takes too long
print(f"Timeout error occurred: {e}")
print("The server took too long to respond")
except requests.exceptions.RequestException as e:
# Base exception for all requests exceptions
print(f"An error occurred: {e}")
except ValueError as e:
# Triggered by response.json() if response isn't valid JSON
print(f"Invalid JSON response: {e}")
This comprehensive error handling demonstrates how to catch and handle different types of exceptions that might occur during HTTP requests:
HTTPError
: Raised when a request returns a bad status code (400-599)ConnectionError
: Raised when there’s a network problem (DNS failure, refused connection, etc.)Timeout
: Raised when a request times outRequestException
: The base exception for all Requests errorsValueError
: Raised when trying to parse invalid JSON
Best Practices for Error Handling
import logging
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def make_api_request(url, method='get', max_retries=3, **kwargs):
"""
Make an API request with retry logic and proper error handling.
Args:
url (str): The URL to send the request to
method (str): HTTP method to use (get, post, put, etc.)
max_retries (int): Maximum number of retry attempts
**kwargs: Additional arguments to pass to requests.request
Returns:
dict: Parsed JSON response
Raises:
requests.exceptions.RequestException: If the request fails after all retries
"""
# Set default timeout if not provided
if 'timeout' not in kwargs:
kwargs['timeout'] = (3.05, 27) # (connect timeout, read timeout)
# Create a session with retry strategy
session = requests.Session()
retry_strategy = Retry(
total=max_retries,
backoff_factor=0.5, # Wait 0.5, 1, 2... seconds between retries
status_forcelist=[429, 500, 502, 503, 504], # Retry on these status codes
allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"] # Methods to retry
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)
try:
logger.info(f"Making {method.upper()} request to {url}")
response = session.request(method, url, **kwargs)
response.raise_for_status()
# Try to parse as JSON, fall back to text if not JSON
try:
return response.json()
except ValueError:
logger.info("Response was not JSON, returning text content")
return {"text_content": response.text}
except requests.exceptions.HTTPError as e:
logger.error(f"HTTP error {e.response.status_code} for {url}: {e}")
# You might want to handle certain status codes differently
if e.response.status_code == 401:
logger.error("Authentication failed. Please check your credentials.")
elif e.response.status_code == 404:
logger.error("Requested resource not found.")
raise
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
logger.error(f"Network error for {url}: {e}")
logger.error("Please check your internet connection and try again.")
raise
except requests.exceptions.RequestException as e:
logger.error(f"Request failed for {url}: {e}")
raise
# Example usage
try:
# Get product data with our robust function
product_data = make_api_request('https://dummyjson.com/products/1')
print(f"Product name: {product_data['title']}")
except requests.exceptions.RequestException as e:
print(f"Could not fetch product data: {e}")
This implementation showcases several best practices:
- Using sessions and retry logic for transient errors
- Setting appropriate timeouts to prevent hanging requests
- Detailed logging for troubleshooting
- Handling different types of errors specifically
- Automatic JSON parsing with fallback to text
- Properly documenting the function with docstrings
Advanced Features
Sessions
If you run into an error with this code make sure the login data is up to date from [https://dummyjson.com/users].
import requests
# Creating and using sessions
with requests.Session() as session:
# Set headers and cookies once for all requests in this session
session.headers.update({
'User-Agent': 'MyApp/1.0',
'Content-Type': 'application/json',
'Accept': 'application/json'
})
# Login to get authenticated
login_data = {'username': 'emilys', 'password': 'emilyspass'}
login_response = session.post('https://dummyjson.com/auth/login', json=login_data)
print
if login_response.status_code == 200:
# The token would normally be extracted from the response
token = login_response.json()['accessToken']
session.headers.update({'Authorization': f'Bearer {token}'})
# Now make authenticated requests using the same session
# Headers, cookies, and connection are persisted between requests
response1 = session.get('https://dummyjson.com/auth/products/1')
print(f"First request: {response1.status_code}")
response2 = session.get('https://dummyjson.com/auth/carts/1')
print(f"Second request: {response2.status_code}")
# You can still override session-level settings for specific requests
response3 = session.get(
'https://dummyjson.com/products/categories',
headers={'Accept-Language': 'en-US'}
)
print(f"Third request (with override): {response3.status_code}")
Sessions provide several benefits:
- Connection pooling – Reuses TCP connections for multiple requests to the same host
- Cookie persistence – Cookies received from the server are automatically used in subsequent requests
- Header persistence – Set headers once for all requests
- Performance improvement – Faster subsequent requests due to connection reuse
Authentication
import requests
# Basic authentication (username and password)
# Note: DummyJSON uses token auth rather than Basic auth, but this is how Basic auth works
response = requests.get(
'https://api.example.com/secure-data',
auth=('username', 'password')
)
print(f"Basic auth status: {response.status_code}")
# Bearer token authentication (common for OAuth 2.0)
# Using DummyJSON's auth endpoint to get a token
login_response = requests.post('https://dummyjson.com/auth/login',
json={'username': 'kminchelle', 'password': '0lelplR'})
if login_response.status_code == 200:
token = login_response.json()['token']
headers = {'Authorization': f'Bearer {token}'}
response = requests.get('https://dummyjson.com/auth/products', headers=headers)
print(f"Bearer auth status: {response.status_code}")
# API key authentication (in query parameters)
params = {'api_key': '1234567890abcdef'}
response = requests.get('https://dummyjson.com/products', params=params)
print(f"API key auth status: {response.status_code}")
# Custom authentication
class CustomAuth(requests.auth.AuthBase):
"""Custom authentication handler for requests"""
def __init__(self, token, auth_type="Token"):
self.token = token
self.auth_type = auth_type
def __call__(self, r):
# Modify the request to include our custom auth header
r.headers['Authorization'] = f'{self.auth_type} {self.token}'
return r
# Use the custom authentication with a token from DummyJSON
login_response = requests.post('https://dummyjson.com/auth/login',
json={'username': 'kminchelle', 'password': '0lelplR'})
if login_response.status_code == 200:
token = login_response.json()['token']
custom_auth = CustomAuth(token=token, auth_type="Bearer")
response = requests.get('https://dummyjson.com/auth/products', auth=custom_auth)
print(f"Custom auth status: {response.status_code}")
This section demonstrates different authentication methods supported by Requests, including:
- Basic authentication (encoded username/password)
- Bearer token authentication (common with OAuth and JWT)
- API keys (via query parameters)
- Custom authentication schemes
Timeouts and Retries
import requests
# Setting timeouts to prevent hanging requests
try:
# (connect timeout, read timeout) in seconds
response = requests.get(
'https://dummyjson.com/products?delay=2', # This endpoint has artificial delay
timeout=(3, 10) # Wait 3 seconds for connection, 10 seconds for response
)
print(f"Request completed in {response.elapsed.total_seconds()} seconds")
except requests.exceptions.Timeout:
print("The request timed out")
# Implementing retries for robust requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Configure retry strategy
retry_strategy = Retry(
total=3, # Maximum number of retries
backoff_factor=1, # Sleep factor between retries (1s, 2s, 4s)
status_forcelist=[429, 500, 502, 503, 504], # Status codes to retry on
allowed_methods=["GET", "POST"] # Methods to apply retry logic to
)
# Create adapter with the retry strategy
adapter = HTTPAdapter(max_retries=retry_strategy)
# Create session and mount the adapter
session = requests.Session()
session.mount("https://", adapter) # Apply adapter to all HTTPS requests
session.mount("http://", adapter) # Apply adapter to all HTTP requests
# Use the session for your requests
try:
# Use a potentially flaky endpoint with delay
response = session.get("https://dummyjson.com/products?delay=2", timeout=5)
print(f"Request succeeded after retries: {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"All retry attempts failed: {e}")
Timeouts prevent your application from hanging indefinitely when servers don’t respond, while retry strategies help overcome transient network issues and temporary server problems.
Best Practices and Tips
Performance Optimization
- Use connection pooling with sessions for multiple requests to the same host
- Set appropriate timeouts to prevent hanging requests
- Use streaming for large files to manage memory usage
- Enable keep-alive connections when possible
import requests
import functools
# Creating an optimized session for high-performance requests
session = requests.Session()
# Configure connection pooling
adapter = requests.adapters.HTTPAdapter(
pool_connections=100, # Number of connections to keep in pool
pool_maxsize=100, # Maximum number of connections in pool
max_retries=3 # Built-in retry configuration
)
session.mount('http://', adapter)
session.mount('https://', adapter)
# Enable keep-alive for connection reuse
session.headers.update({'Connection': 'keep-alive'})
# Set default timeouts for all requests
session.request = functools.partial(session.request, timeout=(3.05, 27))
# Make requests using the optimized session
response = session.get('https://dummyjson.com/products')
This configuration optimizes connection reuse, sets appropriate pool sizes, and ensures all requests have reasonable timeouts.
Security Considerations
- Always verify SSL certificates in production
- Never store credentials in code
- Use environment variables for sensitive data
- Implement rate limiting for API calls
import os
from dotenv import load_dotenv
import requests
# Load credentials from environment variables
load_dotenv()
api_key = os.environ.get('API_KEY')
api_secret = os.environ.get('API_SECRET')
# Verify SSL certificates (default is True, but shown explicitly for emphasis)
response = requests.get(
'https://dummyjson.com/products',
headers={'Authorization': f'Bearer {api_key}'},
verify=True # Verify SSL certificates
)
# For development with self-signed certs, use a certificate file
# response = requests.get('https://local-dev.example', verify='/path/to/certificate.pem')
# WARNING: Never disable verification in production
# requests.get('https://dummyjson.com/products', verify=False) # INSECURE!
# Use session-level certificate verification
session = requests.Session()
session.verify = '/path/to/certificate_bundle.pem' # Use custom CA bundle
Security is critical when making HTTP requests. Always verify SSL certificates in production, avoid hardcoding credentials, and follow security best practices.
Troubleshooting Guide
Common Issues
- SSL Certificate Errors
import certifi
import requests
# Best solution: Update your certificates
print(f"Certificate path: {certifi.where()}")
# Using the certifi CA bundle explicitly
response = requests.get('https://dummyjson.com/products', verify=certifi.where())
# For internal sites with self-signed certificates, use a specific certificate
response = requests.get('https://internal-site.company.com', verify='/path/to/company/cert.pem')
# Temporary fix for development only (NEVER IN PRODUCTION)
# requests.get('https://dummyjson.com/products', verify=False) # INSECURE!
# You'll get a warning which you can suppress (but shouldn't):
# import urllib3
# urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
SSL certificate errors typically occur due to outdated certificates, self-signed certificates, or man-in-the-middle proxies. The proper solution is to update your certificates or provide the correct certificate path.
- Connection Timeouts
# Handling connection timeouts with appropriate values
try:
# connect_timeout, read_timeout in seconds
# Connect timeout: time to establish connection
# Read timeout: time between bytes received
response = requests.get('https://dummyjson.com/products?delay=2', timeout=(3.05, 30))
except requests.exceptions.ConnectTimeout:
print("Failed to connect to the server within the timeout period")
except requests.exceptions.ReadTimeout:
print("Server did not send any data within the timeout period")
except requests.exceptions.Timeout:
print("The request timed out")
# For unreliable connections, implement exponential backoff
import time
import requests
from requests.exceptions import RequestException
def request_with_backoff(url, max_retries=5):
"""Make a request with exponential backoff."""
for attempt in range(max_retries):
try:
response = requests.get(url, timeout=(3.05, 10))
response.raise_for_status()
return response
except RequestException as e:
wait_time = 2 ** attempt # Exponential backoff
print(f"Attempt {attempt + 1}/{max_retries} failed. Retrying in {wait_time}s...")
time.sleep(wait_time)
continue
raise Exception(f"All {max_retries} attempts failed")
# Example usage
try:
response = request_with_backoff('https://dummyjson.com/products?delay=2')
print("Request succeeded after retries")
except Exception as e:
print(f"Failed after multiple attempts: {e}")
Timeouts occur when the server takes too long to respond or when network conditions are poor. Setting appropriate timeout values and implementing retry strategies can help overcome these issues.
Debugging Techniques
Enable debug logging
import requests
import logging
import http.client
# Basic debug logging for requests
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("urllib3").setLevel(logging.DEBUG)
# For even more detailed HTTP connection debugging
http.client.HTTPConnection.debuglevel = 1
# Make a request to see detailed logs
response = requests.get('https://dummyjson.com/products/1')
Enabling debug logging can help identify issues with requests, including headers, connection problems, and SSL errors.
Inspect request and response details
import requests
# Create a prepared request to examine details
session = requests.Session()
request = requests.Request('POST', 'https://dummyjson.com/products/add',
headers={'X-Custom': 'value'},
json={'title': 'New Phone', 'price': 899})
prepared_request = session.prepare_request(request)
# Inspect the prepared request
print(f"Method: {prepared_request.method}")
print(f"URL: {prepared_request.url}")
print(f"Headers: {prepared_request.headers}")
print(f"Body: {prepared_request.body}")
# After sending, examine response details
response = session.send(prepared_request)
print(f"Status: {response.status_code}")
print(f"Response Headers: {response.headers}")
print(f"Encoding: {response.encoding}")
print(f"Elapsed Time: {response.elapsed.total_seconds()}s")
print(f"Redirect History: {[r.status_code for r in response.history]}")
Inspecting the details of requests and responses can help identify issues with headers, cookies, or request formatting.
- Using request hooks for debugging
import requests
def log_request_details(response, *args, **kwargs):
"""Log details about the request-response cycle."""
request = response.request
print(f"--- Request: {request.method} {request.url}")
print(f"Request Headers: {request.headers}")
if request.body:
print(f"Request Body: {request.body[:100]}...")
print(f"--- Response: {response.status_code}")
print(f"Response Headers: {dict(response.headers)}")
print(f"Response Time: {response.elapsed.total_seconds()}s")
return response
# Using the hook with a session
session = requests.Session()
session.hooks['response'] = [log_request_details]
# Now all requests will be logged automatically
response = session.get('https://dummyjson.com/products/1')
Request hooks are a powerful way to inspect and modify requests and responses, making them useful for debugging.
Wrapping Up
The Python Requests library has earned its place as the go-to HTTP client library through its intuitive API and robust features. We’ve covered the essential aspects of making HTTP requests, handling responses, managing errors, and implementing best practices.