API Client Libraries: HTTP Clients, SDKs and Generators

API client libraries explained: HTTP clients vs SDKs, choosing raw, generated or hand-written clients, retries, timeouts, pagination, and OpenAPI generators with costs.

Four stacked layers: your application, an API client library, an HTTP client and the REST API

An API client library is code that calls a web API on behalf of your program. With it, you do not build URLs, set headers and parse JSON by hand on every call. The name covers two different things. An HTTP client, such as fetch or Java’s HttpClient, can talk to any API. An SDK wraps one specific API in typed methods such as devices.get(id). For most teams the sound default depends on the API. Use a plain HTTP client hidden behind one class of your own for a few calls, and a generated client when the API publishes an OpenAPI file. To publish an SDK for others, combine a generated core with a thin hand-written layer. Whichever you pick, the hard work is the same: timeouts, safe retries, pagination, token refresh and clear errors.

This guide compares the main HTTP clients in Java, JavaScript, Python, Go and C#, with versions checked in October 2026, and shows one call written four ways. It then walks through the behavior every production client needs and how to generate a client from an OpenAPI file. The last sections cover what an SDK for your own API should contain and how to test the failure paths that a happy-path test never touches.

1. Two Things Called a Client Library

A comparison that puts axios next to a vendor’s SDK mixes up two layers. An HTTP client library sends a request you describe (a URL, headers and a body) and hands back the raw response. An SDK, short for software development kit, knows one API. It builds the URL, adds the headers, parses the JSON into a Device object and usually uses an HTTP client underneath.

Four stacked layers: your application, an API client library, an HTTP client and the REST API
Your application talks only to the SDK, and the SDK delegates the network work to an HTTP client.

A third kind of tool, the client generator, writes an SDK for you from a machine-readable description of the API. The table puts the three side by side.

KindWhat it doesExamplesWhat you still supply
HTTP client librarySends any HTTP request and returns the response.java.net.http, Spring RestClient, fetch, axios, httpx, Go net/http, .NET HttpClientURLs, headers, bodies and JSON mapping
API SDKOffers typed methods for one API.A Java or JavaScript package published by the API ownerConfiguration and credentials
Client generatorWrites an SDK from an OpenAPI file.OpenAPI Generator, Kiota, openapi-typescriptAn accurate OpenAPI description

1.1. Vocabulary for Reading Client Documentation

Client library documentation leans on a handful of terms. Knowing them makes the rest of this guide, and most release notes, easier to follow.

TermPlain meaning
OpenAPIA machine-readable description of an API: its paths, parameters, and request and response shapes.
Idempotent requestA request with the same effect whether it is sent once or five times, such as PUT or DELETE (RFC 9110). It is safe to retry.
Backoff and jitterWaiting longer after each failed try, plus a random part so thousands of clients do not retry in the same instant.
Connection poolA set of open connections the client reuses instead of opening a new one per request.
Tolerant readerA client that ignores fields it does not know, so a new field in a response does not break it.
Problem DetailsThe standard JSON error body from RFC 9457, sent with the media type application/problem+json.

2. Raw Client, Generated Client or Hand-Written SDK

The real decision is how much code sits between your application and the network, and who maintains it. Each option moves work from one place to another rather than removing it.

ApproachEffort to startTyped modelsOngoing upkeep
Plain HTTP clientLowestNo, you map JSON yourselfYou keep URLs and parsing in step with the API.
Generated clientLowYes, generatedYou regenerate when the spec changes.
Hand-written SDKHighYes, tuned by handEvery API change is a manual edit.
Generated core plus a thin hand-written layerMediumYes, generatedYou regenerate the core and keep your layer small.

Matching the approach to the situation is usually clear once you know the size of the API and who calls it.

  • For a script, a prototype or two or three calls, a plain HTTP client is enough.
  • For a third-party API that publishes an OpenAPI file, a generated client wrapped with your own retry and authentication rules saves days.
  • For a large API that changes often, a generated client rebuilt in CI keeps pace without manual edits.
  • For your own API used by many teams or customers, a generated core plus a hand-written layer, published as a package, works best.
  • For a tiny API with demanding users, a fully hand-written SDK can be worth the upkeep.

If you are unsure, put a plain HTTP client behind one class of your own, for example DeviceApiClient. The rest of the application never sees HTTP, so swapping in a generated client later touches a single file.

3. HTTP Clients by Language, With Current Versions

Libraries rise and fade, so it pays to know what each one is for and whether it is still maintained. The versions below are the latest stable releases on the package registries, checked October 2026. Pin the version you adopt and read its release notes before upgrading.

LanguageLibraryLatest stableWhat to know
Javajava.net.http.HttpClientPart of the JDKIn the JDK since Java 11 (JEP 321); supports HTTP/2 and async calls with no extra dependency.
JavaSpring RestClient and HTTP Service ClientsSpring Framework 7.0.9RestClient is the fluent synchronous client. HTTP Service Clients turn an annotated Java interface into a working client.
JavaSpring WebClientSpring Framework 7.0.9Reactive and non-blocking; it belongs in reactive applications.
JavaSpring RestTemplateSpring Framework 7.0.9The Spring reference documentation marks it deprecated as of 7.0 in favor of RestClient.
JavaApache HttpClient 5, OkHttp5.6.4 and 5.5.0Mature clients with fine control over pooling and TLS.
JavaOpenFeign, RetrofitFeign 13.15, Retrofit 3.0.0You declare an annotated interface and the library implements it.
JavaScriptfetchBuilt inAvailable in browsers and in Node.js 18 and later; timeouts come from AbortSignal.
JavaScriptaxios, ky, got1.20.0, 2.1.0, 16.0.0Convenience layers with interceptors, retries and JSON helpers.
Pythonhttpx0.28.1Sync and async APIs, with a default timeout of 5 seconds.
Pythonrequests2.34.2Very widely used, but it waits forever unless you pass a timeout.
Gonet/http, restyStandard library; resty 2.17.2The standard library is usually enough. Resty adds retries and helpers, and version 3 is in release candidates.
C#HttpClient, Refit.NET built in; Refit 16.3.0Reuse one HttpClient instance; Refit generates a client from an annotated interface.

Two defaults in this table cause real outages. The Python requests library has no timeout unless you pass one, and Spring’s RestTemplate still appears in many tutorials. For new Java code, RestClient is the documented successor.

3.1. Fetching One Device in Java, JavaScript and Python

Each snippet fetches a device by id with an explicit timeout. They are sketches that we have not executed against a live server, so adapt the names to your project and check them in your own build.

// Java, Spring RestClient
RestClient client = RestClient.builder()
    .baseUrl("https://api.example.com")
    .build();

Device device = client.get()
    .uri("/devices/{id}", id)
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
        throw new DeviceApiException(response.getStatusCode());
    })
    .body(Device.class);
// Java, java.net.http
HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(5))
    .build();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/devices/" + id))
    .timeout(Duration.ofSeconds(10))
    .header("Accept", "application/json")
    .GET()
    .build();

HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());
// JavaScript, fetch
const res = await fetch("https://api.example.com/devices/" + encodeURIComponent(id), {
  headers: { Accept: "application/json" },
  signal: AbortSignal.timeout(10000)
});
if (!res.ok) throw new Error("HTTP " + res.status);
const device = await res.json();
# Python, httpx
import httpx

with httpx.Client(base_url="https://api.example.com",
                  timeout=httpx.Timeout(10.0, connect=5.0)) as client:
    r = client.get("/devices/" + device_id,
                   headers={"Accept": "application/json"})
    r.raise_for_status()
    device = r.json()

4. Behavior a Production Client Needs

Sending a request takes a few lines. Clients fail in production for other reasons: a slow partner holds every thread, an HTTP 503 triggers a retry storm, or a token expires halfway through a nightly import. If you use a generated client or an SDK, check each item below and wrap whatever it lacks.

4.1. Timeouts on Connect, Response and the Whole Call

Without a timeout, one slow dependency holds a thread per request until the pool runs dry and your own service stops answering. Set three limits explicitly. A connect timeout covers opening the connection, and a response timeout covers waiting on data. Where the library allows it, add a deadline for the whole call, retries included. Common starting points are 2 to 5 seconds to connect and 10 to 30 seconds for a response. Tune them later from the API’s p99 latency, the time within which 99% of its calls finish.

4.2. Retrying Only When a Second Try Can Succeed

Some failures are temporary: connection resets, timeouts, HTTP 429, HTTP 502, HTTP 503 and HTTP 504. A retry can fix those. Errors such as HTTP 400, HTTP 401, HTTP 403, HTTP 404 and HTTP 422 will fail the same way every time, so retrying them unchanged only adds load.

  • Retry only idempotent methods (GET, HEAD, PUT, DELETE) or a POST that carries an idempotency key. Retrying a plain POST /devices can create two devices.
  • Wait longer after each try and add randomness. For example, wait a random time between zero and 0.5 seconds times two to the power of the attempt number, with a fixed cap.
  • Honor the Retry-After header on HTTP 429 and HTTP 503. If the server asks for 30 seconds, a 2-second wait fails again.
  • Cap both the number of attempts (three or four) and the total time.
  • Retry in one layer only. Three tries in the SDK times three in your wrapper turns one failure into nine calls against an API that is already struggling.

The diagram follows one call through two temporary failures. The caller sees a single method call and a single result.

Sequence diagram: an SDK retries GET /devices/d-1001 after HTTP 503 with a random backoff and after HTTP 429 by waiting the Retry-After value, then returns the device
The retry layer backs off after HTTP 503, waits exactly as long as Retry-After asks after HTTP 429, and returns one result.

This Python sketch applies those rules with httpx. It reads only the seconds form of Retry-After, not the date form.

import random, threading
import httpx

RETRY_STATUS = {429, 502, 503, 504}

def get_with_retry(client, url, attempts=4):
    pause = threading.Event()
    for attempt in range(attempts):
        r = None
        try:
            r = client.get(url)
            if r.status_code not in RETRY_STATUS:
                return r
        except (httpx.ConnectError, httpx.ReadTimeout):
            pass
        wait = random.uniform(0, min(20, 0.5 * 2 ** attempt))
        if r is not None and r.headers.get("Retry-After", "").isdigit():
            wait = int(r.headers["Retry-After"])   # seconds form only
        if attempt < attempts - 1:
            pause.wait(wait)                        # wait before the next try
    raise RuntimeError("device API unavailable after retries")

4.3. Idempotency Keys Make POST Retryable

A POST becomes safe to retry when it carries a unique key, for example Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7. The server stores the result under that key and returns the same result if the key arrives again. The header is defined in an IETF Internet-Draft, not yet an RFC, but several large APIs already accept it. Create the key once per logical operation and reuse it on every retry; our guide to idempotency keys covers the server side.

4.4. Pagination Hidden Behind an Iterator

List endpoints return results one page at a time. A client that reads only the first page loses data without any error, which is why this bug can go unnoticed for weeks. A good client hides pages behind an iterator, so callers loop over devices and never touch a cursor. The sketch assumes each response holds items and a next link.

def devices(client):
    url = "/devices?limit=100"
    while url:
        r = client.get(url)
        r.raise_for_status()
        body = r.json()
        yield from body["items"]
        url = body.get("next")

4.5. Credentials Added in One Place

Authentication bugs usually come from one call that forgot a header or from a refresh that loops. Centralizing the logic fixes both. The schemes themselves are explained in REST API authentication.

  • Add credentials in a single interceptor or request filter, so no call can skip them.
  • When an HTTP 401 arrives, refresh the token once and retry the request once. A refresh loop on a revoked token floods the login service.
  • Read secrets from configuration or a secret store, and keep them out of URLs, logs and source code.

4.6. Status Codes Turned Into Typed Exceptions

Callers handle a DeviceNotFoundException far more cleanly than a generic error with a number inside. Map an HTTP 404 on GET /devices/{id} to DeviceNotFoundException, an HTTP 429 to a RateLimitedException that carries the Retry-After value, and server errors to a ServerException. If the API sends Problem Details, keep its type, title, detail and any request id in the exception. Then a support ticket contains what the API team needs. REST API error handling describes the format.

4.7. Connection Reuse, Logs, Unknown Fields and Version Pins

The last group of habits costs little and prevents slow, hard-to-trace problems.

  • Create one client and reuse it. A new client per call discards the connection pool and repeats the TLS handshake, which shows up as latency under load.
  • Log the method, the route template, the status and the duration, and mask Authorization, API keys and bodies by default.
  • Configure the JSON mapper to ignore unknown properties, so a new response field does not break older clients.
  • Send a User-Agent such as device-sdk-java/2.3.1, so the API owner can see which SDK versions are still in use with REST API analytics tools.
  • Pin the API version the client was built for, in a header or the path as described in REST API versioning. Spring Framework 7 can insert that version for you on RestClient calls.

5. Generating a Client From an OpenAPI File

When an API publishes an OpenAPI file, a generator can write the models and one method per operation for you. On a large API that saves days, and regenerating keeps the client in step with the spec. Output quality differs between generators and between target languages, so try your language on your own spec before committing. Versions and prices below were checked October 2026; confirm prices on the vendor page.

ToolOutputLatest versionCost
OpenAPI GeneratorDozens of client languages7.25.0Free, Apache-2.0
Swagger CodegenMany languages3.0.82Free, Apache-2.0
KiotaC#, Go, Java, PHP, Python, Ruby, TypeScript1.35.0Free, MIT; generates only the endpoints you select
openapi-typescriptTypeScript types and a typed fetch wrapper7.13.0Free, MIT
Hey APITypeScript SDKs0.99.0, still before 1.0Free, MIT
SpeakeasyManaged SDKs in many languagesHosted serviceOnly a tailored enterprise plan is listed.
FernSDKs and documentationHosted serviceA free plan that includes SDKs, and a custom enterprise plan.
StainlessSDKs in many languagesHosted serviceA free plan with up to 5 generators, 5 seats and 25 endpoints; paid tiers are not priced on the page.

With the free OpenAPI Generator, one command turns a spec into a Java client project:

openapi-generator-cli generate -i devices.yaml -g java -o ./device-client

5.1. Spec Habits That Produce Readable Method Names

A generator can only be as clear as the spec it reads. Small habits in the OpenAPI file decide whether users get getDevice or getDevicesIdGet.

  • Give every operation a unique, readable operationId, such as getDevice or listDevices, because generators turn it into the method name.
  • Group operations with tags, so a devices tag becomes a DevicesApi class.
  • Describe every response, including errors, so the generator can create matching exception types.
  • Keep the spec in the same repository as the API code and validate it in CI, as shown in REST API documentation.

5.2. Keeping Generated Files Untouched

Edits inside generated files vanish the next time the generator runs. Put retries, authentication and error mapping in a wrapper class, or in the HTTP client the generated code uses. Pin the generator version so output does not drift between builds, and regenerate in CI whenever the spec changes.

6. Shipping an SDK to Other Teams

If other teams or customers call your API, an SDK saves each of them the work in section 4. It also cuts support questions, because everyone follows the same retry and error rules. The cost is one more product to maintain, so start with the languages your users ask for.

SDK featureWhy users care
Typed models and one method per operationCode completion and compile errors replace guessing at JSON.
Safe defaults for timeouts, retries and paginationMost users never change defaults, so the defaults are the real behavior.
One-line setup of base URL, credentials and timeoutsA first call takes minutes.
A fake transport for testsUsers can test their code without calling your API.
A User-Agent with the SDK name and versionYou can see who still runs an old version before you retire it.
Semantic versions tied to the APIA breaking API change means a new major SDK version, as Semantic Versioning defines.
A changelog and a deprecation policyUsers know what changed and when to upgrade.

Most well-liked SDKs use the hybrid shape. A generated core covers models and endpoints, and a small hand-written layer covers parts that need judgment, such as retries, pagination and authentication. When the API itself must change in a breaking way, breaking changes and API deprecation explains how to give SDK users time to move.

7. Failure Cases Every Client Test Suite Needs

A client has many failure paths, and a happy-path test exercises none of them. Run the client against a stub server you can make misbehave, such as WireMock (3.13.2 is the latest stable release, with 4.0 in beta). Each row is one test worth writing.

CaseWhat the stub doesWhat the client should do
Not foundReturns HTTP 404 with Problem DetailsThrow DeviceNotFoundException with the detail text.
Temporary failureReturns HTTP 503, then HTTP 200Retry once and return the device.
Rate limitReturns HTTP 429 with Retry-After: 2Wait at least 2 seconds before the next try.
Slow responseDelays past the response timeoutFail with a timeout error within the deadline.
Expired tokenReturns HTTP 401, then accepts a new tokenRefresh once and retry once.
Three pagesServes three pages linked by nextReturn every item in order.
New fieldAdds an unknown field to the JSONParse the response without error.

For checks that the client and the API agree on the contract itself, see contract testing with Pact. Broader test design is in REST API testing.

8. Production Symptoms Traced to the Client

Many incidents blamed on the API start in the client. The symptoms on the left are what people report; the cause is usually one missing habit from section 4.

SymptomLikely cause in the clientFix
The service stops answering whenever a partner API is slow.No timeouts, so threads wait forever.Set connect, response and total timeouts.
Duplicate records appear after network blips.A POST is retried without an idempotency key.Send a key, or stop retrying that call.
One outage turns into a flood of calls.Retries are stacked in the SDK, a wrapper and a service mesh.Keep retries in one layer with a cap.
A nightly import finishes with too few records.Only the first page is read.Iterate until there is no next link.
The login service is flooded when tokens expire.The client refreshes in a loop.Refresh once, retry once, then fail.
Latency rises under load with many TLS handshakes.A new client is created per request.Create one client and share it.
Old clients crash after the API adds a field.Strict JSON parsing rejects unknown properties.Ignore unknown fields.

9. Common Questions About Client Libraries

These questions come up whenever a team picks or replaces a client library.

9.1. Should I still use RestTemplate?

Existing code keeps working, but the Spring reference documentation says RestTemplate is deprecated as of Spring Framework 7.0 and will be removed in a future version. Use RestClient for new code, or WebClient in a reactive application. A RestClient can be created from an existing RestTemplate, which allows a gradual move.

9.2. Should I generate the client or write it by hand?

For most teams, generate it and wrap it in a small hand-written layer. Write the whole client by hand only when the API is small or when users need an experience generators cannot produce.

9.3. How do I share one SDK across many services?

Publish it as a package in your internal registry, version it semantically, and let each service pin a version. A shared SDK that changes behavior in place breaks every service at once.

10. Conclusion

A client library hides the repetitive parts of calling an API, but the hard parts stay the same whichever library you choose. Wrap the HTTP client behind one class of your own or generate a client from OpenAPI, and give it timeouts, safe retries, pagination, token refresh and clear errors. Test those failure cases on purpose.

11. References

The specifications, documentation and registries used for the facts and versions in this guide are listed here.

Happy Learning !!

Comments

Subscribe
0 Comments
Most Voted
Newest Oldest