Skip to content

Eden Treaty Client

Eden Treaty provides a fully type-safe client inferred from the API’s Elysia type signature. You get autocomplete on routes, typed request parameters, and typed responses — no code generation or manual type definitions needed.

  1. Install the packages

    Terminal window
    bun add github:CogappLabs/search-api-elysia @elysiajs/eden

    The package is installed as search-api-bun (the npm package name), so imports use that name.

  2. Create a client

    import { treaty } from "@elysiajs/eden";
    import type { App } from "search-api-bun";
    const api = treaty<App>("https://your-search-api.example.com");
  3. Make typed requests

    Eden Treaty maps URL paths to a tree-like object syntax, using . instead of /. Dynamic path parameters like :handle are passed using function call syntax:

    // /:handle/search
    api({ handle: "my-index" }).search.get(/* ... */);
    // /:handle/documents/:id
    api({ handle: "my-index" }).documents({ id: "498" }).get();
    // /:handle/facets/:field
    api({ handle: "my-index" }).facets({ field: "placeCountry" }).get();
    // /health (no dynamic params)
    api.health.get();

Every call returns { data, error, status }. When the request succeeds, data is typed and error is null. When it fails, data is null and error contains the status code and response body:

const { data, error } = await api({ handle: "my-index" }).search.get({
query: { q: "castle" },
});
if (error) {
// error.status is 400 | 404
// error.value is { error: string }
console.error(error.value.error);
} else {
// data is fully typed: hits, totalHits, facets, etc.
console.log(`${data.totalHits} results`);
}
const { data } = await api({ handle: "my-index" }).search.get({
query: {
q: "castle",
page: 1,
perPage: 10,
highlight: "true",
facets: "placeCountry,placeRegion",
},
});
if (data) {
for (const hit of data.hits) {
console.log(hit.objectID, hit._score);
// hit._highlights is Record<string, string[]>
}
// Facets
for (const [field, values] of Object.entries(data.facets)) {
for (const { value, count } of values) {
console.log(`${field}: ${value} (${count})`);
}
}
}

JSON parameters (filters, sort, boosts, histogram, geoGrid) are passed as JSON strings:

const { data } = await api({ handle: "my-index" }).search.get({
query: {
q: "",
filters: JSON.stringify({ placeCountry: ["Scotland", "Wales"] }),
sort: JSON.stringify({ postDate: "desc" }),
perPage: 5,
},
});
const { data } = await api({ handle: "my-index" }).search.get({
query: {
q: "",
histogram: JSON.stringify({ population: 1000 }),
geoGrid: JSON.stringify({
field: "coordinates",
precision: 6,
bounds: {
top_left: { lat: 60, lon: -5 },
bottom_right: { lat: 48, lon: 3 },
},
}),
},
});
if (data) {
// Histogram buckets
for (const [field, buckets] of Object.entries(data.histograms ?? {})) {
for (const { key, count } of buckets) {
console.log(`${field} >= ${key}: ${count}`);
}
}
// Geo clusters
for (const cluster of data.geoClusters ?? []) {
console.log(`${cluster.lat}, ${cluster.lng}: ${cluster.count} docs`);
}
}
const { data } = await api({ handle: "my-index" }).autocomplete.get({
query: {
q: "ston",
perPage: 5,
facets: "placeCountry",
maxFacetsPerField: 4,
},
});
if (data) {
console.log(`${data.totalHits} hits`);
// Facet value matches (values whose names match the query)
if (data.facets) {
for (const [field, values] of Object.entries(data.facets)) {
console.log(field, values);
}
}
}
const { data } = await api({ handle: "my-index" }).documents({ id: "498" }).get();
if (data) {
// data is Record<string, unknown> — the full indexed document
console.log(data.title, data.uri);
}

Search within a facet field’s values, useful for typeahead facet filters:

const { data } = await api({ handle: "my-index" }).facets({ field: "placeCountry" }).get({
query: {
q: "sc",
maxValues: 10,
},
});
if (data) {
for (const { value, count } of data.values) {
console.log(`${value} (${count})`);
}
}

With filters to narrow the facet context:

const { data } = await api({ handle: "my-index" }).facets({ field: "placeRegion" }).get({
query: {
filters: JSON.stringify({ placeCountry: "Scotland" }),
},
});

Retrieve the index mapping to discover available fields:

const { data } = await api({ handle: "my-index" }).mapping.get();
// data is Record<string, unknown>

Send a raw query DSL body directly to the search engine:

const { data } = await api({ handle: "my-index" }).query.post({
query: { match_all: {} },
size: 1,
});
const { data } = await api.health.get();
console.log(data?.status, data?.cache);
  • TypeScript project with moduleResolution set to bundler
  • Bun or Node.js 18+ runtime