Build the mental model
Every lesson so far taught Kibana Dev Tools's shorthand HTTP syntax, but production application code manually building raw `fetch`/`curl` calls with string concatenation is error-prone — URL encoding, JSON serialization, connection pooling, and retry logic all need to be handled by hand. The official `@elastic/elasticsearch` Node.js client abstracts all of that away, and its TypeScript type definitions let you type-check a Query DSL object at compile time — a typo you'd only notice at runtime inside a raw JSON string shows up instantly in your editor instead. Connecting through the client library gives you built-in connection pooling, so you no longer create a new TCP connection per request — share one client instance as a singleton across a Node.js server process. The client's method calls map directly onto Query DSL structures as JavaScript/TypeScript objects, so porting a query from Kibana Dev Tools is straightforward (`GET /tutorials/_search` plus a JSON body becomes `client.search({ index: 'tutorials', query: {...} })`) — this is the payoff of what Lesson 7 promised: Dev Tools syntax ports simply into a client method call.
Connect it to a real scenario
Inside the Tutorial Platform's Next.js API route (`/api/search`), export a module-level singleton client instead of creating a new `Client` instance per request, so every route handler imports and reuses it, sharing a single connection pool per process. Write the Dev Tools `match` plus `bool` query as a TypeScript object literal and `await client.search<TutorialDoc>({ index: 'tutorials', query: {...} })` — passing the generic type parameter `TutorialDoc` lets field-name typos in `hits.hits[].source` get caught at compile time. Pass the Elasticsearch URL and API key from environment variables into the client config, so local dev and production switch through a single configuration.
Try the working example
import { Client } from '@elastic/elasticsearch';
export const esClient = new Client({
node: process.env.ELASTICSEARCH_URL,
auth: { apiKey: process.env.ELASTICSEARCH_API_KEY! },
});
interface TutorialDoc {
title: string;
body: string;
difficulty: string;
}
export async function searchTutorials(term: string) {
const result = await esClient.search<TutorialDoc>({
index: 'tutorials',
query: { match: { body: term } },
});
return result.hits.hits.map((hit) => hit._source);
}You write a type-safe search function that returns an array of tutorial documents with TypeScript types.5-minute try-it
Using the `Client` singleton, write a `searchByDifficulty(term, difficulty)` function that runs a `bool` query (`match` plus `filter`) filtered on `difficulty` — make the return type `TutorialDoc[]`.
One important caution
Instantiating `new Client(...)` again on every API route call or function invocation — losing the shared connection pool and opening excessive TCP connections wastefully.
Leaving `client.search()`'s result untyped as `any` instead of passing the `TutorialDoc` type parameter — a field-name typo (`hits.hits[].source.titl`) is no longer caught at compile time at all.
elasticsearch-js — GitHub — Elastic