Find the right records
Most "how do I query X" questions are one call with the right parameters. This is the mapping, and the two rules that keep it cheap.
Question to endpoint: search, filter and browse
Every row here is one request: searching by caliber, filtering by maker or country, browsing a category. The version people write first, listing everything and filtering in their own code, is the same answer for a hundred times the quota.
| What you want | The call | Notes |
|---|---|---|
| 9mm pistols made in Austria, still in production | GET /v1/firearms?caliber=&category=&country_of_origin=&status= | Filters combine and are applied in the database. Sort with sort and order. |
| Everything chambered in one cartridge | GET /v1/calibers/{id}/firearms | The cartridge-first view, already paged. |
| Everything one maker builds | GET /v1/manufacturers/{id}/firearms | Same shape for a brand page. |
| Everything in a category | GET /v1/categories/{slug}/firearms | Categories holding no firearm are not listed at all. |
| Records carrying a feature (threaded barrel, M-LOK) | GET /v1/firearms/by-feature?feature= | Feature slugs come from filter-options; by-action, by-material, by-designer and by-conflict work the same way. |
| What one country fields, by operator type | GET /v1/countries/{code}/arsenal | Grouped by military, law enforcement and so on. |
| A ranked page for a human to choose from | GET /v1/firearms/search?q= | Full-text over names and descriptions. A ranking, not a decision. |
| Which record a typed name is | GET /v1/firearms/resolve?q= | One id and a status. This is the one to call before fetching a record. |
| The variants of a model | GET /v1/firearms/{id}/variants | Direct children. The family tree endpoint walks further. |
| "You may also like" | GET /v1/firearms/{id}/similar | Ten records scored on shared calibers, maker, action, size and year; images inlined. |
| Leaders on one measurable stat | GET /v1/firearms/top?stat= | Records missing the measurement are excluded rather than ranked as zero. |
| What people are actually looking at | GET /v1/popular/firearms?days= | View activity over a trailing window, for a homepage rail. |
Filter on the server
Every parameter on the list endpoint narrows the query before the page is built, so a narrow filter costs one request and returns one page. The full parameter list is on that page; the point here is that they combine.
# Filters combine, and they are applied in the database.# 9mm semi-automatic pistols still in production, made in Austria,# newest first, a hundred to a page.curl -sS -H "X-API-Key: $GUNSPEC_API_KEY" \ "https://api.gunspec.io/v1/firearms?caliber=9x19mm-parabellum&category=pistol\&country_of_origin=AT&status=in_production&sort=year&order=desc&per_page=100" # Only records that have a photograph or a render, for a page that# is mostly pictures.curl -sS -H "X-API-Key: $GUNSPEC_API_KEY" \ "https://api.gunspec.io/v1/firearms?category=rifle&has_image=true&per_page=24"- Prefer one filtered request to a walk plus a loop. Paging the whole catalog to find eleven rifles is the most expensive way to ask a cheap question, and the plan is where it shows up.
- Ranges are pairs:
weight_min/weight_max,year_introduced_min/year_introduced_max, in the units the field names carry. fieldstrims the response to what you render. A list page that shows a name, a year and a picture does not need seventy fields per row.- Sort deliberately.
sort=created_at&order=ascis the stable order for a walk;nameis the one that shifts under you when a record is added.
Take the vocabulary from the API
One call returns every value that currently has records behind it, which is what a filter UI should offer. Hardcoding the lists is how a dropdown ends up with dead options and misses the category added last month.
// Build the filter UI from the API, not from a constant.// Categories, manufacturers and calibers nothing uses are left out,// so an option in this list always returns results.const { data } = await gunspec.firearms.filterOptions() // data.manufacturers, data.categories, data.calibers,// data.actionTypes, data.features. Countries come from// /v1/countries, which carries the names and codes.//// Hardcoding these is how a UI ends up offering "Flintlock" with// nothing behind it, and missing the category added last month.Autocomplete is two endpoints
Search ranks while the user types; resolve decides once they commit. Using search for both is what puts the wrong firearm on the page, as resolve before you read sets out.
// An autocomplete has two jobs and they are two endpoints.// While the user types: rank plausible records for them to pick.const { data: suggestions } = await gunspec.firearms.search({ q: typed, per_page: 8 }) // Once they pick, or paste a name and hit enter, decide which// record it is and act on the status rather than the ranking.const { data } = await gunspec.firearms.resolve(typed)if (data.status === 'resolved') go(data.firearmId)- Debounce the keystrokes. One request per character is a rate limit you asked for.
- Cache the suggestion list by query string for a few minutes: people retype the same prefixes all day.
- Search needs Builder or above. On Explorer, drive the box from
filter-optionsand the list filters instead.
Listing every caliber, manufacturer, category or country
The reference lists are their own endpoints, each paged the same way, and each cheap enough to fetch once and cache for a day.
GET /v1/calibers,/v1/manufacturers,/v1/categories,/v1/countriesand/v1/ammunitionare the catalogs behind the filters.- Raise
per_pagerather than walking: these lists are hundreds of rows, not thousands, so one or two requests is the whole set. Pagination has the ceilings. - Cache them for a day. A caliber list changes when the catalog gains a cartridge, which is not per request and not per user.
- For a filter UI, prefer
/v1/firearms/filter-options: it returns only the values that currently have records behind them.
Comparing records, variants, family trees and lookalikes
Several questions that look like joins are endpoints of their own, and each returns the records already assembled.
/v1/firearms/{id}/family-treewalks lineage in both directions;/variantsis just the children./v1/firearms/{id}/usersis who fields it, and/adoption-mapthe same thing as geography./v1/firearms/{id}/attachmentsis what fits, and it has rules of its own. See what fits what./v1/firearms/comparetakes up to five ids and returns them aligned, which is one request where fivegetcalls would be five.