| name | customize-sdk-hooks |
| description | Use when implementing SDK code hooks for custom logic (not spec configuration or runtime overrides).
Covers SDK lifecycle hooks: BeforeRequest, AfterSuccess, AfterError for custom headers,
telemetry, HMAC signing, and request/response transformation.
Triggers on "SDK hooks", "add hooks", "BeforeRequestHook", "custom logic in SDK",
"telemetry hook", "SDK middleware", "intercept requests", "HMAC signing hook",
"custom code in hooks directory".
|
| license | Apache-2.0 |
Customize SDK Hooks
When to Use
Use this skill when you need to add custom code logic to the generated SDK:
- Add custom headers (User-Agent, correlation IDs) to every SDK request via code
- Implement telemetry, logging, or observability at the SDK level
- Add custom authentication logic (HMAC signatures, token refresh) that runs in SDK code
- Transform responses or errors before they reach the caller
- Implement custom request/response middleware
- User says: "SDK hooks", "add custom logic", "intercept requests with code", "HMAC signing hook", "telemetry in SDK"
NOT for:
- OpenAPI spec modifications (see
manage-openapi-overlays)
- Runtime SDK client config (see
configure-sdk-options)
Inputs
- Hook type: Which lifecycle event to intercept (init, before request, after success, after error)
- SDK language: The target language of the generated SDK (TypeScript, Go, Python, Java, C#, Ruby, PHP, etc.)
- Custom logic: The behavior to inject at the hook point
Outputs
- Hook implementation file(s) in
src/hooks/ (or language equivalent)
- Updated
src/hooks/registration.ts to register the new hook
- The hook is preserved across SDK regenerations
Prerequisites
- A Speakeasy-generated SDK (any supported language)
- Understanding of the SDK's request/response lifecycle
- The
src/hooks/ directory exists in the generated SDK (created by default)
Hook Types
| Hook | When Called | Common Use Cases |
|---|
SDKInitHook | SDK client initialization | Configure defaults, validate config, set base URL |
BeforeCreateRequestHook | Before the HTTP request object is created | Modify input parameters, inject defaults |
BeforeRequestHook | Before the HTTP request is sent | Add headers, logging, telemetry, sign requests |
AfterSuccessHook | After a successful HTTP response | Transform response, emit warnings, log metrics |
AfterErrorHook | After an HTTP error response | Error transformation, retry logic, error logging |
Hook Interfaces (TypeScript)
interface SDKInitHook {
sdkInit(opts: SDKInitOptions): SDKInitOptions;
}
interface BeforeCreateRequestHook {
beforeCreateRequest(hookCtx: BeforeCreateRequestHookContext, input: any): any;
}
interface BeforeRequestHook {
beforeRequest(
hookCtx: BeforeRequestHookContext,
request: Request
): Request;
}
interface AfterSuccessHook {
afterSuccess(
hookCtx: AfterSuccessHookContext,
response: Response
): Response;
}
interface AfterErrorHook {
afterError(
hookCtx: AfterErrorHookContext,
response: Response | null,
error: unknown
): { response: Response | null; error: unknown };
}
Directory Structure
src/
hooks/
types.ts # Generated - DO NOT EDIT (hook interfaces/types)
registration.ts # Custom - YOUR registrations (preserved on regen)
custom_useragent.ts # Custom - your hook implementations
telemetry.ts # Custom - your hook implementations
Key rule: registration.ts and any custom hook files you create are preserved
during SDK regeneration. The types.ts file is regenerated and should not be modified.
Registration Pattern
All hooks are registered in src/hooks/registration.ts. This file is created once
by the generator and never overwritten. You add your hooks here:
import { Hooks } from "./types.js";
import { CustomUserAgentHook } from "./custom_useragent.js";
import { TelemetryHook } from "./telemetry.js";
export function initHooks(hooks: Hooks) {
hooks.registerBeforeRequestHook(new CustomUserAgentHook());
hooks.registerAfterSuccessHook(new TelemetryHook());
}
Command
To add a hook to a Speakeasy-generated SDK:
- Create your hook implementation file in
src/hooks/
- Implement the appropriate interface from
src/hooks/types.ts
- Register it in
src/hooks/registration.ts
- Regenerate the SDK -- your hooks are preserved
speakeasy generate sdk -s openapi.yaml -o . -l typescript
Examples
Example 1: Custom User-Agent Hook
Add a custom User-Agent header to every outgoing request.
import {
BeforeRequestHook,
BeforeRequestHookContext,
} from "./types.js";
export class CustomUserAgentHook implements BeforeRequestHook {
private userAgent: string;
constructor(appName: string, appVersion: string) {
this.userAgent = `${appName}/${appVersion}`;
}
beforeRequest(
hookCtx: BeforeRequestHookContext,
request: Request
): Request {
const newRequest = new Request(request, {
headers: new Headers(request.headers),
});
newRequest.headers.set("User-Agent", this.userAgent);
const existing = request.headers.get("User-Agent");
if (existing) {
newRequest.headers.set(
"User-Agent",
`${this.userAgent} ${existing}`
);
}
return newRequest;
}
}
Register it:
import { Hooks } from "./types.js";
import { CustomUserAgentHook } from "./custom_useragent.js";
export function initHooks(hooks: Hooks) {
hooks.registerBeforeRequestHook(
new CustomUserAgentHook("my-app", "1.0.0")
);
}
Example 2: Custom Security Hook (HMAC Signing)
For APIs requiring HMAC signatures or custom authentication that cannot be
expressed in the OpenAPI spec, combine an overlay with a BeforeRequestHook.
Step 1: Use an overlay to mark the security scheme so Speakeasy generates
the hook point:
overlay: 1.0.0
info:
title: Add HMAC security
actions:
- target: "$.components.securitySchemes"
update:
hmac_auth:
type: http
scheme: custom
x-speakeasy-custom-security: true
Step 2: Implement the signing hook:
import {
BeforeRequestHook,
BeforeRequestHookContext,
} from "./types.js";
import { createHmac } from "crypto";
export class HmacSigningHook implements BeforeRequestHook {
beforeRequest(
hookCtx: BeforeRequestHookContext,
request: Request
): Request {
const timestamp = Date.now().toString();
const secret = hookCtx.securitySource?.apiSecret;
if (!secret) {
throw new Error("API secret is required for HMAC signing");
}
const signature = createHmac("sha256", secret)
.update(`${request.method}:${request.url}:${timestamp}`)
.digest("hex");
const newRequest = new Request(request, {
headers: new Headers(request.headers),
});
newRequest.headers.set("X-Timestamp", timestamp);
newRequest.headers.set("X-Signature", signature);
return newRequest;
}
}
Register it:
import { Hooks } from "./types.js";
import { HmacSigningHook } from "./hmac_signing.js";
export function initHooks(hooks: Hooks) {
hooks.registerBeforeRequestHook(new HmacSigningHook());
}
Best Practices
-
Keep hooks focused: Each hook should address a single concern. Use separate
hooks for user-agent, telemetry, and auth rather than one monolithic hook.
-
Clone responses before reading the body: The Response.body stream can only
be consumed once. Always clone before reading:
afterSuccess(hookCtx: AfterSuccessHookContext, response: Response): Response {
const cloned = response.clone();
cloned.json().then((data) => console.log("Response:", data));
return response;
}
-
Fire-and-forget for telemetry: Do not block the request pipeline for
non-critical operations like logging or metrics:
beforeRequest(hookCtx: BeforeRequestHookContext, request: Request): Request {
void fetch("https://telemetry.example.com/events", {
method: "POST",
body: JSON.stringify({ operation: hookCtx.operationID }),
});
return request;
}
-
Test hooks independently: Write unit tests for hooks in isolation by
constructing mock Request/Response objects and hook contexts.
-
Use hookCtx.operationID: The hook context provides the current operation
ID, which is useful for per-operation behavior, logging, and metrics.
What NOT to Do
- Do NOT edit
types.ts: This file is regenerated. Your changes will be lost.
- Do NOT consume the response body without cloning: This causes downstream
failures because the body stream is exhausted.
- Do NOT perform blocking I/O in hooks: Long-running operations (network calls,
file I/O) in hooks will degrade SDK performance. Use fire-and-forget patterns
for non-critical work.
- Do NOT throw errors in
AfterSuccessHook unless you intend to convert a
success into a failure. Throwing in hooks disrupts the normal flow.
- Do NOT store mutable shared state in hooks without synchronization. Hooks may
be called concurrently in multi-threaded environments.
- Do NOT duplicate logic that belongs in an OpenAPI overlay. If you need to
modify the API spec (add security schemes, change parameters), use an overlay
instead of a hook.
Troubleshooting
Hook is not being called
- Verify the hook is registered in
src/hooks/registration.ts
- Confirm you are registering for the correct hook type (e.g.,
registerBeforeRequestHook
vs registerAfterSuccessHook)
- Check that
initHooks is exported and follows the expected signature
Response body is empty or already consumed
- You are reading
response.body or calling response.json() without cloning first
- Always use
response.clone() before consuming the body, then return the original
Hooks lost after regeneration
- Custom hook files in
src/hooks/ are preserved, but only if they are separate files
registration.ts is never overwritten after initial generation
types.ts IS overwritten -- never put custom code there
TypeScript compilation errors after regeneration
types.ts may have updated interfaces. Check for breaking changes in hook signatures
- Update your hook implementations to match the new interface definitions
Hook causes request failures
- Ensure you are returning a valid
Request or Response object
- Check that cloned requests preserve the original body and headers
- Verify any injected headers have valid values (no undefined or null)
Other Languages
While the examples above are in TypeScript, Speakeasy SDK hooks are available
across all supported languages:
- Go: Hooks implement interfaces in
hooks/hooks.go with registration in
hooks/registration.go
- Python: Hooks are classes in
src/hooks/ implementing protocols from
src/hooks/types.py
- Java: Hooks implement interfaces from
hooks/SDKHooks.java
- C#: Hooks implement interfaces from
Hooks/SDKHooks.cs
- Ruby: Hooks use Sorbet-typed classes with Faraday middleware patterns
- PHP: Hooks use PSR-7 request/response interfaces with Guzzle middleware
The hook types, lifecycle, and registration pattern are consistent across all
languages. Refer to the generated types file in your SDK for language-specific
interface definitions.