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.

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.
| Kind | What it does | Examples | What you still supply |
|---|---|---|---|
| HTTP client library | Sends any HTTP request and returns the response. | java.net.http, Spring RestClient, fetch, axios, httpx, Go net/http, .NET HttpClient | URLs, headers, bodies and JSON mapping |
| API SDK | Offers typed methods for one API. | A Java or JavaScript package published by the API owner | Configuration and credentials |
| Client generator | Writes an SDK from an OpenAPI file. | OpenAPI Generator, Kiota, openapi-typescript | An 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.
| Term | Plain meaning |
|---|---|
| OpenAPI | A machine-readable description of an API: its paths, parameters, and request and response shapes. |
| Idempotent request | A 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 jitter | Waiting longer after each failed try, plus a random part so thousands of clients do not retry in the same instant. |
| Connection pool | A set of open connections the client reuses instead of opening a new one per request. |
| Tolerant reader | A client that ignores fields it does not know, so a new field in a response does not break it. |
| Problem Details | The 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.
| Approach | Effort to start | Typed models | Ongoing upkeep |
|---|---|---|---|
| Plain HTTP client | Lowest | No, you map JSON yourself | You keep URLs and parsing in step with the API. |
| Generated client | Low | Yes, generated | You regenerate when the spec changes. |
| Hand-written SDK | High | Yes, tuned by hand | Every API change is a manual edit. |
| Generated core plus a thin hand-written layer | Medium | Yes, generated | You 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.
| Language | Library | Latest stable | What to know |
|---|---|---|---|
| Java | java.net.http.HttpClient | Part of the JDK | In the JDK since Java 11 (JEP 321); supports HTTP/2 and async calls with no extra dependency. |
| Java | Spring RestClient and HTTP Service Clients | Spring Framework 7.0.9 | RestClient is the fluent synchronous client. HTTP Service Clients turn an annotated Java interface into a working client. |
| Java | Spring WebClient | Spring Framework 7.0.9 | Reactive and non-blocking; it belongs in reactive applications. |
| Java | Spring RestTemplate | Spring Framework 7.0.9 | The Spring reference documentation marks it deprecated as of 7.0 in favor of RestClient. |
| Java | Apache HttpClient 5, OkHttp | 5.6.4 and 5.5.0 | Mature clients with fine control over pooling and TLS. |
| Java | OpenFeign, Retrofit | Feign 13.15, Retrofit 3.0.0 | You declare an annotated interface and the library implements it. |
| JavaScript | fetch | Built in | Available in browsers and in Node.js 18 and later; timeouts come from AbortSignal. |
| JavaScript | axios, ky, got | 1.20.0, 2.1.0, 16.0.0 | Convenience layers with interceptors, retries and JSON helpers. |
| Python | httpx | 0.28.1 | Sync and async APIs, with a default timeout of 5 seconds. |
| Python | requests | 2.34.2 | Very widely used, but it waits forever unless you pass a timeout. |
| Go | net/http, resty | Standard library; resty 2.17.2 | The 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.0 | Reuse 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.

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.
| Tool | Output | Latest version | Cost |
|---|---|---|---|
| OpenAPI Generator | Dozens of client languages | 7.25.0 | Free, Apache-2.0 |
| Swagger Codegen | Many languages | 3.0.82 | Free, Apache-2.0 |
| Kiota | C#, Go, Java, PHP, Python, Ruby, TypeScript | 1.35.0 | Free, MIT; generates only the endpoints you select |
| openapi-typescript | TypeScript types and a typed fetch wrapper | 7.13.0 | Free, MIT |
| Hey API | TypeScript SDKs | 0.99.0, still before 1.0 | Free, MIT |
| Speakeasy | Managed SDKs in many languages | Hosted service | Only a tailored enterprise plan is listed. |
| Fern | SDKs and documentation | Hosted service | A free plan that includes SDKs, and a custom enterprise plan. |
| Stainless | SDKs in many languages | Hosted service | A 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 feature | Why users care |
|---|---|
| Typed models and one method per operation | Code completion and compile errors replace guessing at JSON. |
| Safe defaults for timeouts, retries and pagination | Most users never change defaults, so the defaults are the real behavior. |
| One-line setup of base URL, credentials and timeouts | A first call takes minutes. |
| A fake transport for tests | Users can test their code without calling your API. |
| A User-Agent with the SDK name and version | You can see who still runs an old version before you retire it. |
| Semantic versions tied to the API | A breaking API change means a new major SDK version, as Semantic Versioning defines. |
| A changelog and a deprecation policy | Users 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.
| Case | What the stub does | What the client should do |
|---|---|---|
| Not found | Returns HTTP 404 with Problem Details | Throw DeviceNotFoundException with the detail text. |
| Temporary failure | Returns HTTP 503, then HTTP 200 | Retry once and return the device. |
| Rate limit | Returns HTTP 429 with Retry-After: 2 | Wait at least 2 seconds before the next try. |
| Slow response | Delays past the response timeout | Fail with a timeout error within the deadline. |
| Expired token | Returns HTTP 401, then accepts a new token | Refresh once and retry once. |
| Three pages | Serves three pages linked by next | Return every item in order. |
| New field | Adds an unknown field to the JSON | Parse 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.
| Symptom | Likely cause in the client | Fix |
|---|---|---|
| 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.
- Spring Framework: REST Clients
- JEP 321: HTTP Client and java.net.http.HttpClient
- MDN: Fetch API and MDN: AbortSignal.timeout()
- HTTPX timeouts
- .NET HttpClient guidelines
- OpenAPI Generator and Kiota overview
- RFC 9110: HTTP Semantics and RFC 9457: Problem Details for HTTP APIs
- IETF draft: The Idempotency-Key HTTP Header Field
- Semantic Versioning and WireMock
Happy Learning !!
Comments