# HTTP::API::Core [![CPAN version](https://img.shields.io/cpan/v/HTTP-API-Core.svg)](https://metacpan.org/dist/HTTP-API-Core) [![CI](https://github.com/kawamurashingo/HTTP-API-Core/actions/workflows/test.yml/badge.svg)](https://github.com/kawamurashingo/HTTP-API-Core/actions/workflows/test.yml) [![Perl](https://img.shields.io/badge/perl-5.10%2B-blue.svg)](https://www.perl.org/) [![CPAN Testers](https://img.shields.io/badge/CPAN-Testers-blue.svg)](https://www.cpantesters.org/distro/H/HTTP-API-Core.html) [![License](https://img.shields.io/badge/license-Perl%205-blue.svg)](https://dev.perl.org/licenses/) **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. ```perl 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: ```perl 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: ```console cpanm HTTP::API::Core ``` Or with the CPAN client: ```console cpan HTTP::API::Core ``` For development from a checkout: ```console perl Makefile.PL make make test ``` See the [distribution on MetaCPAN](https://metacpan.org/dist/HTTP-API-Core) for release information and generated module documentation. Then create a client: ```perl 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: ```perl 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 metadata * `HTTP::API::Core::Example::Slack` — cursor pagination using `response_metadata.next_cursor` * `HTTP::API::Core::Example::Cloudflare` — page-number pagination using `result_info.total_pages` See [docs/REAL_API_EXAMPLES.md](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: ```perl 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: ```perl 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. See [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md). ### Pagination Next-URL, page-number, and cursor pagination share one iterator interface: ```perl 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: ```perl 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: * `encode` * `decode` * `transport` * `http` * `hook` Application code can inspect fields such as `category`, `status`, `retryable`, and `request_id` instead of parsing human-readable messages. See [docs/ERRORS.md](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: ```perl say $response->elapsed; say $response->request_id if defined $response->request_id; ``` ### Idempotency Supply an idempotency key without assuming a service-specific header: ```perl 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](docs/IDEMPOTENCY.md). ### Transport adapters Use the `transport` option to integrate another HTTP implementation: ```perl 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](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](docs/REFERENCE.md). ## Response API Response handling is explicit and predictable: ```perl $response->status; $response->headers; $response->header('content-type'); $response->content; $response->text; $response->content_type; $response->is_json; $response->json; ``` See [docs/RESPONSE.md](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](DESIGN.md) for the full project direction and criteria for 1.0. ## License Same terms as Perl itself.