Dynamic authentication

Your API may require dynamic authentication where credentials need to be generated or refreshed for each request, such as signing short-lived JWTs or rotating tokens. TypeScript SDKs support this pattern through custom fetcher middleware.

Custom fetcher middleware

The recommended way to implement dynamic authentication in TypeScript SDKs is to use a custom fetcher. This acts as middleware for all requests, allowing you to inject authentication logic in a single place without overriding individual methods.

How it works

When you enable allowCustomFetcher in your generator configuration, the generated SDK accepts a fetcher parameter in the client options. This fetcher wraps all HTTP requests, giving you a single injection point for authentication logic.

Example: Short-lived JWT signing

Here’s how to implement JWT signing with token memoization:

1

Enable custom fetcher in generator configuration

Add allowCustomFetcher: true to your generators.yml:

generators.yml
1default-group: local
2groups:
3 local:
4 generators:
5 - name: fernapi/fern-typescript-node-sdk
6 version: 0.x.x
7 output:
8 location: local-file-system
9 path: ../generated/typescript
10 config:
11 allowCustomFetcher: true
2

Create a custom fetcher with JWT signing

Create a fetcher function that wraps the default fetcher and injects JWT authentication:

src/wrapper/jwtFetcher.ts
1import * as jwt from "jsonwebtoken";
2import { fetcher as defaultFetcher, type FetchFunction } from "../core/fetcher";
3
4export function createJwtFetcher(privateKey: string): FetchFunction {
5 // Cache token to avoid regenerating on every request
6 let cachedToken: { value: string; expiresAt: number } | undefined;
7
8 return async (args) => {
9 const now = Math.floor(Date.now() / 1000);
10
11 // Regenerate token if expired or about to expire (within 2 seconds)
12 if (!cachedToken || cachedToken.expiresAt - 2 <= now) {
13 const payload = {
14 iat: now,
15 exp: now + 15, // Token valid for 15 seconds
16 };
17 const token = jwt.sign(payload, privateKey, { algorithm: "RS256" });
18 cachedToken = { value: token, expiresAt: payload.exp };
19 }
20
21 // Inject JWT into request headers
22 const headers = {
23 ...(args.headers ?? {}),
24 Authorization: `Bearer ${cachedToken.value}`,
25 };
26
27 // Call the default fetcher with modified headers
28 return defaultFetcher({ ...args, headers });
29 };
30}
3

Create a wrapper client

Extend the generated client to use the custom fetcher:

src/wrapper/PlantStoreClient.ts
1import { PlantStoreClient as FernClient } from "../Client";
2import { createJwtFetcher } from "./jwtFetcher";
3
4// Infer the exact options type from the generated client's constructor
5type FernClientOptions = ConstructorParameters<typeof FernClient>[0];
6// Accept all options except 'fetcher', and add our 'privateKey'
7type Options = Omit<FernClientOptions, "fetcher"> & { privateKey: string };
8
9export class PlantStoreClient extends FernClient {
10 constructor(options: Options) {
11 // Extract privateKey and pass all other options to the parent
12 const { privateKey, ...clientOptions } = options;
13 super({
14 ...clientOptions,
15 fetcher: createJwtFetcher(privateKey),
16 });
17 }
18}

This pattern uses ConstructorParameters to infer the exact options type from the generated client, ensuring compatibility with all client options (headers, timeoutInSeconds, maxRetries, etc.) without hardcoding them. This keeps the wrapper future-proof as the generator adds new options.

4

Export the wrapper client

Update your index.ts to export the wrapper instead of the generated client:

src/index.ts
1export { PlantStoreClient } from "./wrapper/PlantStoreClient";
2export * from "./api"; // Export types
5

Add to .fernignore

Protect your custom code from being overwritten:

.fernignore
1+ src/wrapper
2+ src/index.ts
6

Use the client

Users can use the client with automatic JWT signing on all requests:

1import { PlantStoreClient } from "plant-store-sdk";
2
3const client = new PlantStoreClient({
4 privateKey: process.env.PRIVATE_KEY,
5 environment: "https://api.plantstore.com",
6});
7
8// JWT is automatically signed and injected for each request
9const plant = await client.plants.get("monstera-123");
10const newPlant = await client.plants.create({
11 name: "Fiddle Leaf Fig",
12 species: "Ficus lyrata"
13});

Other authentication patterns

This same pattern works for other dynamic authentication scenarios:

  • OAuth token refresh: Automatically refresh expired access tokens before each request
  • HMAC (Hash-based Message Authentication Code) signing: Sign requests with HMAC signatures based on request content
  • Rotating API keys: Switch between multiple API keys based on rate limits
  • Time-based tokens: Generate tokens that include timestamps or nonces

Important considerations

Custom fetcher requirements

  • Generator configuration: The allowCustomFetcher option must be enabled in your generators.yml for the fetcher parameter to be available in BaseClientOptions
  • Import path: Import the default fetcher from ../core/fetcher (or the appropriate path in your generated SDK) to wrap it with your custom logic
  • Preserve all arguments: When wrapping the default fetcher, ensure you pass through all arguments to maintain compatibility with the SDK’s internal behavior

Security considerations

  • Server-side only: Never expose private keys in browser environments. JWT signing with private keys should only be done in server-side code (Node.js, Deno, Bun)
  • Secure key storage: Never hardcode private keys; use environment variables or secure key management systems
  • Avoid double authentication: If your API already uses bearer token authentication in the Fern definition, be careful not to override the existing Authorization header. Consider using a different header name or conditionally setting the header

Performance and concurrency

  • Token memoization: Cache tokens to avoid regenerating them on every request. The example above caches tokens and refreshes them 2 seconds before expiration
  • Thread safety: The memoization pattern shown is safe for concurrent requests in JavaScript’s single-threaded event loop, but be mindful of race conditions in other environments
  • Grace period: Refresh tokens slightly before they expire (e.g., 2 seconds early) to avoid edge cases where a token expires during request processing

Header merging

  • Preserve existing headers: When injecting authentication headers, always spread existing headers to avoid overwriting headers set by the SDK or user
  • Header precedence: Headers are merged in order: SDK defaults → client options → request options → custom fetcher. Your custom fetcher runs last and can override previous headers

Time synchronization

  • Clock drift: Be aware of potential clock drift between your client and server. Consider adding tolerance to your token expiration checks
  • Timestamp precision: Use Unix timestamps (seconds since epoch) for iat and exp claims to match JWT standards

Best practices

  • Cache tokens appropriately: Balance between security (shorter token lifetime) and performance (less frequent regeneration)
  • Handle errors gracefully: Implement retry logic for authentication failures and token refresh errors
  • Test thoroughly: Ensure your wrapper handles all edge cases, including concurrent requests, token expiration, and network failures
  • Monitor token usage: Log token generation and refresh events to help debug authentication issues in production

See also