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, and the source is on GitHub. It works with both import and require.
npm install tiny-platesThe same package also gives you the tiny-plates command. See Command line.
Your first request
You need an API key. Sign up and create one on your dashboard. 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. Referencepopular.list()The most popular recipes. Referencerecipes.byId(id, { servings, units })One recipe, scaled and converted if you ask. Referencerecipes.list(filters)Filter every recipe. Business plan only. Referencepantry.list({ ingredients })Recipes ranked by how much your pantry covers. ReferenceshoppingList.build(recipes)One shopping list for several recipes. Referencesubstitutions.list(ingredients)What to use instead of an ingredient. Referenceingredients.resolve(names)Ingredient names, in any language, to ids. Referenceingredients.byId(id)One ingredient. Referenceingredients.pairings(id)Ingredients that go well with this one. Referenceingredients.list()Every ingredient. Business plan only. Referenceingredients.vocabularies()The values you can filter on. Referencestatus.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. 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, 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. 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, 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 has the rest.