---
title: "JavaScript client — Tiny Plates recipe API"
description: "Call the Tiny Plates recipe API from JavaScript or TypeScript with the tiny-plates package: typed methods, errors you can check, retries and paging."
url: "https://www.tinyplates.dev/docs/javascript"
---

# JavaScript client

The tiny-plates package wraps every endpoint in a typed method. It has no runtime dependencies, and it works in Node 20 or later, Bun and anywhere else with fetch.

## Install

The package is [tiny-plates on npm](https://www.npmjs.com/package/tiny-plates), and the source is [on GitHub](https://github.com/Artmann/tiny-plates-js). It works with both `import` and `require`.

```
npm install tiny-plates
```

The same package also gives you the `tiny-plates` command. See [Command line](https://www.tinyplates.dev/docs/cli).

## Your first request

You need an API key. [Sign up](https://www.tinyplates.dev/sign-up) and create one on your [dashboard](https://www.tinyplates.dev/dashboard/keys). A free key comes with 50 requests a day. Keep it out of your code, for example in a `TINY_PLATES_API_KEY` environment variable.

```
import { createClient } from 'tiny-plates'

const tinyPlates = createClient({
  apiKey: process.env.TINY_PLATES_API_KEY ?? ''
})

const { results } = await tinyPlates.search({ query: 'carbonara', limit: 5 })

for (const result of results) {
  console.log(result.title)
}
```

`createClient` throws if the key is empty, so a missing environment variable shows up straight away. Every response type is exported, so you can write `import type { Recipe }` in your own code.

## Methods

There is one method per endpoint. Lists, numbers and flags are typed, so you pass `ingredients: ['egg', 'tomato']` rather than a comma-separated string.

* `search({ query })`Search recipes by text. [Reference](https://www.tinyplates.dev/docs/api/search-recipes)
* `popular.list()`The most popular recipes. [Reference](https://www.tinyplates.dev/docs/api/popular-recipes)
* `recipes.byId(id, { servings, units })`One recipe, scaled and converted if you ask. [Reference](https://www.tinyplates.dev/docs/api/get-recipe)
* `recipes.list(filters)`Filter every recipe. Business plan only. [Reference](https://www.tinyplates.dev/docs/api/list-recipes)
* `pantry.list({ ingredients })`Recipes ranked by how much your pantry covers. [Reference](https://www.tinyplates.dev/docs/api/pantry-recipes)
* `shoppingList.build(recipes)`One shopping list for several recipes. [Reference](https://www.tinyplates.dev/docs/api/shopping-list)
* `substitutions.list(ingredients)`What to use instead of an ingredient. [Reference](https://www.tinyplates.dev/docs/api/substitutions)
* `ingredients.resolve(names)`Ingredient names, in any language, to ids. [Reference](https://www.tinyplates.dev/docs/api/resolve-ingredients)
* `ingredients.byId(id)`One ingredient. [Reference](https://www.tinyplates.dev/docs/api/get-ingredient)
* `ingredients.pairings(id)`Ingredients that go well with this one. [Reference](https://www.tinyplates.dev/docs/api/ingredient-pairings)
* `ingredients.list()`Every ingredient. Business plan only. [Reference](https://www.tinyplates.dev/docs/api/list-ingredients)
* `ingredients.vocabularies()`The values you can filter on. [Reference](https://www.tinyplates.dev/docs/api/vocabularies)
* `status.read()`How many recipes and sites there are.
* `health.check()`Whether the API is up. Needs no key.

A few things you can do with them:

```
// Scale a recipe for four people, in metric.
const { recipe } = await tinyPlates.recipes.byId('carbonara', {
  servings: 4,
  units: 'metric'
})

// What can I cook? Missing at most one ingredient, and no milk.
const { recipes } = await tinyPlates.pantry.list({
  ingredients: ['egg', 'tomato', 'onion'],
  maxMissing: 1,
  without: ['milk']
})

// One shopping list for two recipes.
const list = await tinyPlates.shoppingList.build([
  { id: 'carbonara', servings: 4 },
  { id: 'caesar-salad' }
])
```

## Pages

`popular`, `pantry` and `recipes` answer in [pages](https://www.tinyplates.dev/docs/pagination). Their `list` method returns one page and takes `limit` and `offset`. Their `iterate` method walks every page for you, 100 recipes per request:

```
for await (const recipe of tinyPlates.pantry.iterate({
  ingredients: ['egg', 'tomato'],
  maxMissing: 1
})) {
  console.log(recipe.title, recipe.coverage)
}
```

Each page is a request and counts towards your [limit](https://www.tinyplates.dev/docs/rate-limits), so break out of the loop once you have what you need.

## Errors

Every error the client throws extends `TinyPlatesError`. Check the kind with `instanceof`, or read `code` for the exact reason from the [list of errors](https://www.tinyplates.dev/docs/errors). The message says what went wrong and what to do about it, so you can show it to people.

```
import { NotFoundError, RateLimitError } from 'tiny-plates'

try {
  const { recipe } = await tinyPlates.recipes.byId('carbonara')

  console.log(recipe.title)
} catch (error) {
  if (error instanceof NotFoundError) {
    console.log('No such recipe.')
  } else if (error instanceof RateLimitError) {
    console.log(`Try again in ${error.retryAfter} seconds.`)
  } else {
    throw error
  }
}
```

The kinds are `AuthenticationError`, `PlanRequiredError`, `RateLimitError`, `NotFoundError`, `InvalidRequestError`, `InternalError`, `ConnectionError` and `UnexpectedResponseError`. Internal and connection errors are retried twice, after 500 ms and then 1 s. A rate limit is not retried, because the wait can be hours. `retryAfter` says how many seconds are left.

## Options

Only `apiKey` is required. `baseUrl`, `fetch`, `maxRetries` (2 by default) and `timeout` (10 seconds, in milliseconds) are there when you need them.

```
const tinyPlates = createClient({
  apiKey: process.env.TINY_PLATES_API_KEY ?? '',
  maxRetries: 2,
  timeout: 10_000
})

// Cancel a request with an AbortSignal as the last argument.
const controller = new AbortController()
const search = tinyPlates.search(
  { query: 'soup' },
  { signal: controller.signal }
)
```

## Effect

If you use [Effect](https://effect.website/), import from `tiny-plates/effect` and install `effect` 4 next to the package. The client is a service with the same methods. Errors are tagged values in the error channel, and each method lists only the errors its endpoint can return. Every response is checked against the schema, and paged lists have `stream` instead of `iterate`.

```
import { Effect, Layer } from 'effect'
import { FetchHttpClient } from 'effect/http'
import { TinyPlates } from 'tiny-plates/effect'

const program = Effect.gen(function* () {
  const tinyPlates = yield* TinyPlates
  const { recipe } = yield* tinyPlates.recipes.byId('carbonara')

  return recipe.title
}).pipe(
  Effect.catchTag('NotFoundError', () => Effect.succeed('No such recipe.'))
)

const layer = TinyPlates.layerConfig().pipe(
  Layer.provide(FetchHttpClient.layer)
)

Effect.runPromise(Effect.provide(program, layer))
```

`TinyPlates.layerConfig()` reads the key from the `TINY_PLATES_API_KEY` config value. To pass it yourself, use `TinyPlates.layer({ apiKey: Redacted.make(key) })`. The [README](https://github.com/Artmann/tiny-plates-js) has the rest.
