Listings client
@product-comparison/listings-client is the typed client and contract of the public API. It wraps every route, serialises query arguments and rejects with a typed error. The platform imports the same package to validate requests, so the types always match what is served.
"private": true, version 0.0.1) and is not published to npm. Consume it as a workspace dependency ("@product-comparison/listings-client": "workspace:*") from an app in this repository, such as apps/frontend.Entry points
| Import | Contents |
|---|---|
@product-comparison/listings-client | createListingsClient, ListingsError, isListingsError, the response types (PublicProduct, PublicOffer, Page<T>, Money and more), the query types and the outbound helpers (isBotUserAgent, referrerHostOf, OUTBOUND_REDIRECT_HEADERS). |
@product-comparison/listings-client/schema | The zod schemas of every query string, for example productListQuerySchema, publicSearchQuerySchema and clickReportSchema. |
Create a client
The token is a server-side secret. Create the client in server code only.
// server/utils/listings.ts (a Nuxt site)
import { createListingsClient } from '@product-comparison/listings-client'
export const listings = () => createListingsClient({
baseURL: useRuntimeConfig().listingsBaseUrl, // https://platform.example
token: useRuntimeConfig().listingsToken, // NUXT_LISTINGS_TOKEN
locale: 'nb-NO',
})
Options
| Option | Description |
|---|---|
baseURL | Platform origin, for example https://platform.example. The client adds /api/public/v1. |
token | The website's pwt_… token. |
locale | Default ?locale for every GET request. A call's own locale wins. |
fetch | A custom fetch, for tests or Workers service bindings. |
userAgent | Overrides the default product-comparison/listings-client. |
Methods
| Method | Route | Returns |
|---|---|---|
website(query?) | GET /website | PublicWebsite |
brands.list(query?) | GET /brands | Page<PublicBrandListItem> |
brands.get(slug, query?) | GET /brands/:slug | PublicBrand |
products.list(query?) | GET /products | Page<PublicProductListItem> |
products.get(slug, query?) | GET /products/:slug | PublicProduct |
products.similar(slug, query?) | GET /products/:slug/similar | PublicProductListItem[] |
products.priceHistory(slug, query?) | GET /products/:slug/price-history | PublicPriceHistory |
merchants.list(query?) | GET /merchants | Page<PublicMerchantListItem> |
merchants.get(slug, query?) | GET /merchants/:slug | PublicMerchant |
sitemap(query?) | GET /sitemap | PublicSitemap |
search(query) | GET /search | PublicSearchResult |
outbound.resolve(offerId, s) | GET /go/:offerId | PublicOutboundResolution |
outbound.reportClicks(clicks) | POST /clicks | PublicClickReportResult |
search accepts either a string or { q, types, limit }. Query arguments are the partial output types of the schemas, so defaults apply when you leave a field out. The parameters are listed on the public API page.
const listings = createListingsClient({ baseURL, token, locale: 'nb-NO' })
const site = await listings.website()
const page = await listings.products.list({ brand: 'xqs', sort: 'minPrice', pageSize: 12 })
const product = await listings.products.get('ettan-loos')
const history = await listings.products.priceHistory('ettan-loos', { days: 30 })
const found = await listings.search({ q: 'tropical', types: ['product', 'brand'] })
outbound.resolve and outbound.reportClicks are meant for self-hosted redirect clients, not for rendering pages. See Click tracking.
Handle errors
Every method rejects with a ListingsError. It never contains the token.
| Property | Description |
|---|---|
code | unauthorized, not_found, validation, rate_limited, network or unknown. Mapped from the status: 401 and 403 to unauthorized, 404 to not_found, 400 and 422 to validation, 429 to rate_limited. |
status | The HTTP status when the server answered. |
data | The data of the error response. |
replacedBySlug | Set on a 404 for a product, brand or merchant that was merged into another one. |
import { isListingsError } from '@product-comparison/listings-client'
export default defineEventHandler(async (event) => {
try {
return await listings().products.get(getRouterParam(event, 'slug')!)
} catch (error) {
// Merged products: redirect permanently to the surviving one.
if (isListingsError(error) && error.replacedBySlug) {
return sendRedirect(event, `/products/${error.replacedBySlug}`, 301)
}
throw error
}
})
Use the schemas
The /schema entry exports the zod schemas the platform validates requests with. Use them to validate your own incoming query strings with the same names, defaults and limits:
import { productListQuerySchema } from '@product-comparison/listings-client/schema'
const query = productListQuerySchema.parse(getQuery(event))
It also exports MAX_CLICK_BATCH (100), MAX_CLICKS_PER_ENTRY (1000) and clickReportSchema.
Related helpers
@product-comparison/catalog-kitprovidesapplyTitleTemplate, JSON-LD builders andbuildSitemapXmlfor rendering what the API returns.@product-comparison/redirectis a self-hostable redirect client built on this package.