HTTP::API::Core
Production-ready API client plumbing for Perl — without replacing your HTTP stack.
Retries, pagination, rate limits, authentication, structured errors, JSON handling, observability, and idempotency in one small, dependency-light core.
use HTTP::API::Core;
my $api = HTTP::API::Core->new(
base_url => 'https://api.example.com',
);
my $users = $api->get('/users')->json;
Keep using HTTP::Tiny, LWP, Mojo::UserAgent, Furl, or your preferred transport. HTTP::API::Core sits above it and centralizes the policy that otherwise gets reimplemented in every API client.
You write the service-specific methods. HTTP::API::Core handles the plumbing.
Is this for me?
Use HTTP::API::Core when you are building an API client or small SDK and do not want to reimplement the same plumbing for every service:
- JSON request and response handling
- query parameter encoding
- structured errors
- safe retries and
Retry-After - rate-limit handling
- next-URL, page-number, and cursor pagination
- authentication hooks
- request IDs and timing
- idempotency keys
- transport adapters
You still write the small, service-specific methods that make your client useful:
sub get_user {
my ($self, $id) = @_;
return $self->{api}->get("/users/$id")->json;
}
The core handles the policy around that request.
Quick start
Install the latest release from CPAN:
cpanm HTTP::API::Core
Or with the CPAN client:
cpan HTTP::API::Core
For development from a checkout:
perl Makefile.PL
make
make test
See the distribution on MetaCPAN for release information and generated module documentation.
Then create a client:
use HTTP::API::Core;
my $api = HTTP::API::Core->new(
base_url => 'https://api.example.com',
headers => {
Authorization => "Bearer $ENV{API_TOKEN}",
},
timeout => 10,
retry => {
attempts => 3,
base_delay => 0.25,
max_delay => 5,
jitter => 1,
},
);
my $response = $api->get('/users');
my $data = $response->json;
Why not just call the HTTP client directly?
An API wrapper often starts simple:
my $response = $http->get($url);
Then production requirements arrive: encode parameters, decode JSON, normalize failures, retry transient errors, respect rate limits, paginate, attach authentication, capture request IDs, and make the whole thing testable.
HTTP::API::Core provides those common pieces without becoming a service-specific SDK or a new HTTP stack.
Common use cases
Building a GitHub-like API client? Use page-number pagination and normalized rate-limit metadata.
Building a Slack-like API client? Use cursor pagination without writing the iteration loop yourself.
Calling an unreliable API? Configure conservative retries with exponential backoff, jitter, and Retry-After support.
Building an internal SDK? Keep authentication, structured errors, logging/tracing hooks, and transport details out of your resource methods.
Real API examples
Tested examples show how the same core maps onto APIs with different conventions:
HTTP::API::Core::Example::GitHub— page-number pagination over a top-level JSON array, plus GitHub rate-limit metadataHTTP::API::Core::Example::Slack— cursor pagination usingresponse_metadata.next_cursorHTTP::API::Core::Example::Cloudflare— page-number pagination usingresult_info.total_pages
See docs/REAL_API_EXAMPLES.md.
These are integration recipes, not official SDKs for those services.
Features at a glance
Query parameters
Pass a hash reference instead of building query strings by hand:
my $response = $api->get('/users',
query => {
state => 'active',
tag => ['admin', 'staff'],
after => undef,
},
);
Values are percent-encoded, array references generate repeated keys, undefined values are omitted, and existing query strings and fragments are handled correctly.
Authentication
Authentication helpers are implemented as before_request hooks:
use HTTP::API::Core::Auth qw(bearer_auth);
my $api = HTTP::API::Core->new(
base_url => 'https://api.example.com',
hooks => {
before_request => bearer_auth($token),
},
);
Bearer tokens, HTTP Basic authentication, API-key headers, and API-key query parameters are supported. OAuth token acquisition and refresh deliberately remain outside the core.
Pagination
Next-URL, page-number, and cursor pagination share one iterator interface:
my $pager = $api->paginate(
'/users',
mode => 'cursor',
items => 'data.users',
next => 'meta.next_cursor',
query => { limit => 100 },
);
while (my $user = $pager->next) {
...
}
Extractors may be dotted paths or coderefs. Repeated next URLs or cursors are rejected instead of looping forever.
Retries and rate limits
Retries are intentionally conservative. By default, only GET, HEAD, PUT, DELETE, and OPTIONS are retried.
Retryable failures include transport errors, HTTP 408, 425, 429, 5xx, and exhausted-quota 403 responses. Delays use exponential backoff with jitter; Retry-After delay-seconds or HTTP-date values take precedence when available.
Responses expose normalized rate-limit metadata:
my $rate = $response->rate_limit;
say $rate->remaining if defined $rate->remaining;
say $rate->wait_seconds if $rate->exhausted;
Structured errors
Failures use HTTP::API::Core::Error with machine-readable categories:
encodedecodetransporthttphook
Application code can inspect fields such as category, status, retryable, and request_id instead of parsing human-readable messages.
See docs/ERRORS.md.
Hooks and observability
Client-level and per-request hooks let you add authentication, logging, metrics, tracing, or other cross-cutting behavior without subclassing.
Responses expose transport elapsed time and common request IDs:
say $response->elapsed;
say $response->request_id if defined $response->request_id;
Idempotency
Supply an idempotency key without assuming a service-specific header:
my $response = $api->post(
'/payments',
json => { amount => 1000 },
idempotency => {
key => $key,
header => 'Idempotency-Key',
},
);
The core does not generate keys automatically or make unsafe methods retryable implicitly.
See docs/IDEMPOTENCY.md.
Transport adapters
Use the transport option to integrate another HTTP implementation:
my $api = HTTP::API::Core->new(
base_url => 'https://api.example.com',
transport => My::Transport->new(...),
);
Adapters can be coderefs or objects with a request method. Transport exceptions and malformed results become structured transport errors.
See docs/TRANSPORT.md.
Detailed reference
For the complete behavioral notes and examples—including hooks, observability, rate-limit semantics, all pagination modes, retry policy, response handling, errors, idempotency, and transport adapters—see docs/REFERENCE.md.
Response API
Response handling is explicit and predictable:
$response->status;
$response->headers;
$response->header('content-type');
$response->content;
$response->text;
$response->content_type;
$response->is_json;
$response->json;
See docs/RESPONSE.md.
Scope and project direction
HTTP::API::Core aims to stay small, predictable, dependency-light, transport-independent, and safe for production use.
Service-specific SDK behavior, complete OAuth flows, OpenAPI generation, GraphQL-specific clients, WebSockets, HTTP server functionality, and async runtime concerns intentionally remain outside the core.
See DESIGN.md for the full project direction and criteria for 1.0.
License
Same terms as Perl itself.