Skip to Content
Course content

414: Building a REST API Client Tool

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Listen, in a professional codebase, you'll rarely see requests.get() calls scattered randomly throughout the business logic. It's messy, it makes testing a nightmare, and if the API provider changes their base URL or authentication scheme, you're stuck with a massive find-and-replace mission. Instead, we build a "Client"—a dedicated wrapper class that encapsulates the API's quirks.

For this lesson, we're going to build a small tool to pull data from the GitHub API. I want to be able to fetch basic repository information and a list of open issues without worrying about the underlying HTTP headers every single time.

Designing the Client Wrapper

I always start by creating a class that handles the "plumbing." This includes the base URL and any authentication tokens. By putting these in the __init__ method, the rest of the class can just focus on the specific endpoints.

import requests

class GitHubClient:
    def __init__(self, token=None):
        self.base_url = "https://api.github.com"
        self.session = requests.Session()
        if token:
            # Using a session is much more efficient for multiple requests
            self.session.headers.update({"Authorization": f"token {token}"})
        self.session.headers.update({"Accept": "application/vnd.github.v3+json"})

I used requests.Session() here instead of just calling requests.get(). It's a habit I've picked up over the years; sessions reuse the underlying TCP connection, which makes your tool significantly faster when you're making multiple calls to the same host.

Handling the Request Logic

Now, I need a generic way to make requests. I don't want to write try-except blocks inside every single method I add to this class. I'll create a private helper method to handle the actual network call.

    def _request(self, method, endpoint, params=None):
        url = f"{self.base_url}/{endpoint}"
        response = self.session.request(method, url, params=params)
        return response.json()

    def get_repo_details(self, owner, repo):
        endpoint = f"repos/{owner}/{repo}"
        return self._request("GET", endpoint)

Fixing the "Happy Path" Assumption

Here is where I usually mess up when I'm rushing—and I did it just now. I wrote the _request method assuming the API will always return a 200 OK. But what happens if the repo doesn't exist? Or if the token expires? GitHub will return a 404 or 401, and while .json() might still work, the data inside will be an error message, not the repo details. My get_repo_details method would then return an error dictionary, and the calling code would probably crash with a KeyError when trying to access a field like 'stargazers_count'.

I need to actually check the status code. I'll use raise_for_status() to turn those HTTP errors into Python exceptions that we can actually handle.

    def _request(self, method, endpoint, params=None):
        url = f"{self.base_url}/{endpoint}"
        response = self.session.request(method, url, params=params)
        
        # This is the fix: if the status is 4xx or 5xx, it raises an HTTPError
        response.raise_for_status() 
        
        return response.json()

Adding Resource-Specific Methods

Now that the plumbing is robust, adding new functionality is trivial. I want to be able to get open issues for a repo. I can just leverage the _request method and pass in some query parameters.

    def get_open_issues(self, owner, repo):
        endpoint = f"repos/{owner}/{repo}/issues"
        params = {"state": "open"}
        return self._request("GET", endpoint, params=params)

# Let's put it to work
client = GitHubClient()
try:
    repo_info = client.get_repo_details("psf", "requests")
    print(f"Stars: {repo_info['stargazers_count']}")
    
    issues = client.get_open_issues("psf", "requests")
    print(f"Open Issues: {len(issues)}")
except requests.exceptions.HTTPError as e:
    print(f"API Error occurred: {e}")

By structuring it this way, the "user" of the GitHubClient class doesn't need to know about URLs, headers, or the requests library itself. They just call a method and get data back. That's the essence of a clean API client.




📋 Practical Task

Exercise: Building a Space-X Launch Data Client

Using the patterns from this lesson, create a Python class called SpaceXClient that interacts with the public SpaceX API (https://api.spacexdata.com/v4). Your client should meet the following requirements:

  • Initialize with the base URL in the __init__ method.
  • Implement a private _request method that uses a requests.Session and includes response.raise_for_status() to handle errors.
  • Implement a public method get_latest_launch() that fetches data from the /launches/latest endpoint.
  • Implement a public method get_launches_by_flight_number(flight_number) that fetches data from the /launches/{flight_number} endpoint.

Test your client by printing the name of the latest launch and the details of flight number 1. Ensure you wrap your test calls in a try-except block to catch potential HTTPError exceptions.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.