GunSpec
Practice

SDK patterns in TypeScript and Python

The wiring around the calls: one client per process, retries and revalidation configured once, auto-paging, typed errors and raw bytes.

Both SDKs read the key from the environment, so it never passes through your own code. Build the client at module scope and share it: each instance keeps its own connection pool, retry budget and ETag store, and a client per request throws all three away every time. The SDK reference documents every method.

client
TypeScript
import { GunSpec, MemoryETagStore } from '@buun_group/gunspec-sdk' // One client per process, built once and shared. Each instance keeps// its own connection pool, retry budget and ETag store, so a client// per request throws all three away on every request.export const gunspec = new GunSpec({  apiKey: process.env.GUNSPEC_API_KEY,  timeout: 10_000,  // Transient failures only, with backoff and jitter. Terminal  // answers are never retried.  retry: { maxRetries: 3 },  // Revalidation, handled for you: a repeat read sends  // If-None-Match and a 304 costs nothing against the daily cap.  etagCache: new MemoryETagStore(),})
  • Set a timeout. The default is generous, and a request that never returns is worse than one that fails.
  • etagCache turns repeat reads into conditional requests, and a 304 is not counted against the daily cap.
  • Python ships a sync client and an async client with the same surface, so a FastAPI service and a cron script share one integration.
  • The key still belongs on a server. Neither SDK makes a browser safe, which keys in production covers.

Both SDKs walk pages for you and stop on a short page, so the walk behaves the same on plans that report a total and plans that do not. Hand-written loops that test against totalPages read exactly one page on Explorer. Pagination has the per-plan ceilings.

paging
TypeScript
The SDK walks the pages. It stops on a short page, so it works
1/2
// on every plan, including the ones that are not told the total.for await (const firearm of gunspec.firearms.listAutoPaging({ category: 'rifle', per_page: 100 })) {  await upsert(firearm)}

Every failure is an exception carrying reason, status, details and requestId. Branch on the reason, log the request id, and let handling failure decide what is worth retrying.

errors
TypeScript
import { APIError, RateLimitError } from '@buun_group/gunspec-sdk' try {  return await gunspec.firearms.getAttachments(id)} catch (err) {  if (err instanceof RateLimitError) return retryLater(err)  if (err instanceof APIError) {    // Branch on reason, never on the message. Both are always    // present, and requestId is what support needs.    if (err.reason === 'PLAN_REQUIRED') return upsellFitment()    log.error({ reason: err.reason, requestId: err.requestId }, 'gunspec')  }  throw err}

A few responses are bytes: the 3D model, image derivatives, the SVG drawings. Those come back as raw responses carrying the body, the content type and the URL the bytes came from after any CDN redirect. Rendering the artwork covers which representation to ask for.

raw-bytes
TypeScript
// Some responses are files rather than JSON: the 3D model, an// image derivative, the bullet drawing. Those return raw bytes with// the content type and the final URL after any CDN redirect.const model = await gunspec.firearms.getModel('glock-g17')await writeFile('glock-g17.glb', model.body)console.log(model.contentType, model.body.byteLength)

Four common shapes, and the one rule they share: the key stays on the server side of your own boundary.

RuntimeHow to wire it
Next.js or RemixModule-scope client in a route handler or server action. Never import the SDK into a client component.
FastAPI or DjangoOne client on startup. Async client for FastAPI, sync for Django views and management commands.
Cloudflare Workers or other edge runtimesBuild the client per request from the binding secret; edge isolates are short-lived, so keep the ETag store external.
Scheduled jobs and importersSync client, auto-paging, generous timeout, and a cursor stored between runs as in mirroring the catalog.