Intend: Architecture of an Intent-Driven API Client

"Every time you send a request, you're typing the same headers. What if the client just knew?"


The spark

I was studying how HTTP headers get resolved — Content-Type, Authorization, the Sec-Fetch-* family — and realized: for most API calls, headers are entirely derivable from the method, payload shape, and auth strategy.

Then I looked at how existing clients work. Postman, Insomnia, HTTPie — all script-based. They hand you a text box and say "fill it in." None of them actually reason about what headers your request needs.

So I thought: what if headers were resolved dynamically? What if you expressed your intent — POST this JSON with Bearer auth — and the system derived the protocol for you?

That's Intend.


Core abstraction

Everything starts with one record:

public record RequestIntent(
    Method method,
    URI url,
    Object payload,
    AuthStrategy auth,
    boolean forceNew,
    String env
) {}

Six fields. No headers. Just intent.


The header engine

The heart of Intend. Takes a ResolutionContext and runs it through an ordered pipeline of HeaderProvider plugins.

The SPI contract

public interface HeaderProvider {
    int getOrder();
    boolean supports(ResolutionContext context);
    HeaderResolution resolve(ResolutionContext context);
}

Three methods. getOrder() sets priority. supports() gates activation. resolve() produces headers.

The pipeline

public Map<String, String> execute(ResolutionContext context) {
    Map<String, String> headers = new LinkedHashMap<>();
    for (HeaderProvider provider : providers) {
        if (provider.supports(context)) {
            HeaderResolution r = provider.resolve(context);
            if (r.success()) headers.putAll(r.headers());
        }
    }
    return headers;
}

Five built-in providers:

OrderProviderActivates whenProduces
10ProtocolProviderAlwaysContent-Type, Accept, User-Agent
50IdempotencyProviderPOST / PUT / PATCHIdempotency-Key, X-Request-ID
90ApiKeyProviderAuth = API_KEYX-API-KEY
90BasicAuthProviderAuth = BASIC_AUTHAuthorization: Basic
91BearerTokenProviderAuth = BEARER_TOKENAuthorization: Bearer

Want to add a new header? Implement HeaderProvider, give it an order, done. Zero changes to core.


Content-Type auto-detection

The ProtocolProvider inspects the payload:

if (payload.toString().trim().startsWith("{") 
    || payload.toString().trim().startsWith("[")) {
    headers.put("Content-Type", "application/json");
} else if (payload.toString().trim().startsWith("<")) {
    headers.put("Content-Type", "application/xml");
} else {
    headers.put("Content-Type", "text/plain");
}

Starts with { → JSON. Starts with < → XML. Otherwise plain text. Deterministic, correct for 99% of use cases.


Data flow

Rendering diagram…

Template engine

URLs and bodies support {{placeholder}} expressions resolved before execution:

PlaceholderOutput
{{uuid}}550e8400-e29b-41d4-...
{{timestamp}}2026-03-02T14:30:00Z
{{randomInt}}427
{{randomEmail}}user_3847@example.com

Response chaining

Define a capture rule: USER_ID=/id

Response: {"id": "abc-123"} → stores USER_ID = abc-123

Next request: https://api.example.com/users/{{USER_ID}} → resolves to .../users/abc-123

No copy-pasting. No environment variables.


Authentication

Declarative. Pick a strategy from a dropdown. Credentials come from environment config:

{
  "devUrl": "http://localhost:8080",
  "devKey": "dev-api-key-here",
  "prodUrl": "https://api.example.com",
  "prodKey": "prod-api-key-here"
}

Switch environments → credentials switch automatically.


Execution layer

Built on java.net.http.HttpClient:

SettingValue
HTTP versionHTTP/2 with fallback
Connect timeout10s
Request timeout30s
Streaming> 10 MB triggers disk download

Result:

public record ExecutionResult(
    int statusCode,
    String body,
    long timeMs,
    long sizeBytes,
    String statusCategory,
    Map<String, String> requestHeaders
) {}

Persistence

Everything local. No cloud. No accounts.

~/.intend/
├── history.json            # Request history
├── intend-config.json      # Environment config  
└── intend-state.properties # Idempotency keys

Delete ~/.intend/ → clean slate.


DI wiring

Providers are simple value objects — manually instantiated in a @Configuration class:

@Bean
public HeaderEngine headerEngine(StateRepository stateRepo) {
    return new HeaderEngine(List.of(
        new ProtocolProvider(),
        new ApiKeyProvider(),
        new BasicAuthProvider(),
        new BearerTokenProvider(),
        new IdempotencyProvider(stateRepo)
    ));
}

Order in this list doesn't matter — HeaderEngine re-sorts by getOrder().


The takeaway

Every API client treats headers as a user responsibility. Intend treats them as a system responsibility. Declare intent. Get protocol.


Built with intent.