Fastify tutorial
In this tutorial, you’ll build Bargain Chef, a standalone Genkit backend on Fastify that exposes a recipe-generating flow over HTTP. It uses two AI patterns Genkit simplifies: streaming structured output and tool calling.
What you’ll build
Section titled “What you’ll build”For each request, your server prompts Gemini to draft a recipe, and the model calls a tool to look up mock grocery sale prices so it can prefer on-sale ingredients. The server streams the recipe back field-by-field as it’s generated, so clients see progress before the full recipe is ready.
You can find the finished code on GitHub.
Prerequisites
Section titled “Prerequisites”- Node.js v20 or later
- npm
- Familiarity with Fastify and TypeScript
Set up the application
Section titled “Set up the application”Create the project
Section titled “Create the project”Create a new Fastify project:
mkdir my-genkit-fastifycd my-genkit-fastifynpm init -ynpm pkg set type=modulenpx tsc --initmkdir -p src/genkittouch src/index.ts src/genkit/bargainChefFlow.tsmkdir my-genkit-fastifycd my-genkit-fastifypnpm init -ynpm pkg set type=modulepnpm dlx tsc --initmkdir -p src/genkittouch src/index.ts src/genkit/bargainChefFlow.tsmkdir my-genkit-fastifycd my-genkit-fastifyyarn create -ynpm pkg set type=moduleyarn dlx tsc --initmkdir -p src/genkittouch src/index.ts src/genkit/bargainChefFlow.tsmkdir my-genkit-fastifycd my-genkit-fastifybun create -ynpm pkg set type=modulebunx tsc --initmkdir -p src/genkittouch src/index.ts src/genkit/bargainChefFlow.tsInstall packages
Section titled “Install packages”Install the packages you need:
npm install fastify @fastify/cors genkit @genkit-ai/google-genai @genkit-ai/fastifynpm install -D typescript tsx @types/node genkit-clipnpm add fastify @fastify/cors genkit @genkit-ai/google-genai @genkit-ai/fastifypnpm add -D typescript tsx @types/node genkit-cliyarn add fastify @fastify/cors genkit @genkit-ai/google-genai @genkit-ai/fastifyyarn add -D typescript tsx @types/node genkit-clibun add fastify @fastify/cors genkit @genkit-ai/google-genai @genkit-ai/fastifybun add -d typescript tsx @types/node genkit-cliThese packages include:
fastify: The Fastify web framework.@fastify/cors: Fastify CORS plugin. Lets browser frontends served from a different origin call the Genkit endpoint.genkit: Core Genkit SDK.@genkit-ai/google-genai: Plugin that connects Genkit to Google’s Gemini models.@genkit-ai/fastify: Exposes a Genkit flow as a Fastify route, including server-sent events for streaming.genkit-cli: CLI tool that enables Genkit testing and observability.
Configure a model API key
Section titled “Configure a model API key”This tutorial uses the Gemini API from Google AI Studio:
Get a Gemini API Key
Set the GEMINI_API_KEY environment variable to your key:
export GEMINI_API_KEY=<your API key>Create the backend
Section titled “Create the backend”The backend handles requests from any client that calls your HTTP endpoint. For each request, it prompts Gemini to draft a recipe, lets the model call a tool to look up today’s grocery sale prices, and streams the partial recipe back to the caller as it’s generated.
The whole pipeline is a single Genkit flow. A flow is a special Genkit function with built-in observability, type safety, and tooling integration.
You’ll build the backend in four parts:
- Initialize Genkit and register Gemini as the model provider.
- Define a tool the model can call to fetch sale prices.
- Describe the recipe shape with Zod so Genkit can validate the final output and stream partial recipe chunks.
- Define the flow that ties everything together.
Create src/genkit/bargainChefFlow.ts:
import { googleAI } from '@genkit-ai/google-genai';import { genkit, z } from 'genkit';
const ai = genkit({ plugins: [googleAI()],});
const getIngredientsOnSale = ai.defineTool( { name: 'getIngredientsOnSale', description: 'Returns the ingredients on sale at the local grocery store, with prices. The sale set differs between weekdays and weekends.', inputSchema: z.object({ dayType: z .enum(['weekday', 'weekend']) .describe('Whether to fetch weekday or weekend sale prices.'), }), outputSchema: z.array( z.object({ name: z.string(), price: z.string(), }), ), }, async ({ dayType }) => { // Mock data: in a real app, query a pricing database. return dayType === 'weekend' ? [ { name: 'chicken breast', price: '$2.99/lb' }, { name: 'pasta', price: '$0.79' }, { name: 'canned tomatoes', price: '$0.99' }, { name: 'garlic', price: '$0.50 / head' }, { name: 'olive oil', price: '$6.99' }, ] : [ { name: 'eggs', price: '$3.49 / dozen' }, { name: 'spinach', price: '$1.99' }, { name: 'parmesan', price: '$4.99' }, { name: 'lemons', price: '$0.50 each' }, { name: 'rice', price: '$2.49' }, { name: 'butter', price: '$3.99' }, ]; },);
const RecipeSchema = z.object({ title: z.string(), description: z.string(), servings: z.number(), ingredients: z.array( z.object({ name: z.string(), quantity: z.string(), onSale: z.boolean(), }), ), steps: z.array(z.string()),});
export const bargainChefFlow = ai.defineFlow( { name: 'bargainChefFlow', inputSchema: z.object({ craving: z .string() .describe('What the user feels like eating right now.'), }), outputSchema: RecipeSchema, streamSchema: RecipeSchema.partial(), }, async ({ craving }, { sendChunk }) => { const today = new Date().toLocaleDateString('en-US', { weekday: 'long' });
const { stream, response } = ai.generateStream({ model: googleAI.model('gemini-flash-latest', { temperature: 0.7, thinkingConfig: { thinkingLevel: 'MINIMAL' }, }), prompt: `Today is ${today}. The user is craving: ${craving}.
Call the getIngredientsOnSale tool with the dayType that matches today. Saturday and Sunday are weekends; all other days are weekdays. Then propose ONE recipe that takes advantage of those deals. For each ingredient, set onSale=true if it appears in the tool's response, false otherwise.`, tools: [getIngredientsOnSale], output: { schema: RecipeSchema }, });
for await (const chunk of stream) { if (chunk.output) sendChunk(chunk.output); }
const { output } = await response; if (!output) throw new Error('Failed to generate recipe'); return output; },);A few details are worth noting:
- Final output and streamed chunks:
outputSchemais the complete recipe the flow returns at the end.streamSchemais the same shape with every field optional (RecipeSchema.partial()), because early chunks might only include the title or description. - The
getIngredientsOnSaletool: The model decides when to call it based on the prompt, and the typedinputSchemaforces the model to passdayType: 'weekday'or'weekend'. In a real app, the tool would query a pricing database, inventory system, or third-party API. sendChunk: Each call forwards the latest partial recipe to the caller, so the response fills in field by field as the model generates it.
Add the server route
Section titled “Add the server route”Wire up the Genkit backend in src/index.ts. The @genkit-ai/fastify plugin’s fastifyHandler adapts the flow to a Fastify route, so there’s no request or streaming plumbing to write.
import { fastifyHandler } from '@genkit-ai/fastify';import cors from '@fastify/cors';import Fastify from 'fastify';import { bargainChefFlow } from './genkit/bargainChefFlow.js';
const app = Fastify({ logger: true });await app.register(cors, { origin: true });app.post('/bargainChefFlow', fastifyHandler(bargainChefFlow));
await app.listen({ port: 3000, host: '0.0.0.0' });fastifyHandler(bargainChefFlow) parses the JSON request body, invokes the flow, and (when the client opts in with an Accept: text/event-stream header) streams chunks back as server-sent events. It handles the Fastify-to-Genkit bridging for you, including copying CORS headers onto the streamed response so browsers accept it.
cors, { origin: true } reflects the request origin, so any browser frontend can call this endpoint during development. Before deploying, restrict it to the origins you actually serve (for example, { origin: 'https://your-app.com' }).
Check the project layout
Section titled “Check the project layout”Verify that your project layout matches the structure below:
- package.json
- tsconfig.json
Directorysrc
Directorygenkit
- bargainChefFlow.ts
- index.ts
Optional: install the Genkit agent skills
Section titled “Optional: install the Genkit agent skills”If you’re coding with an AI assistant, install the Genkit Agent Skills so it has structured guidance on Genkit APIs, patterns, and common errors:
npx skills add genkit-ai/skillsSee Develop with AI for tool-specific installation instructions.
Run the app
Section titled “Run the app”Start the Fastify server:
npx tsx --watch src/index.tspnpm dlx tsx --watch src/index.tsyarn dlx tsx --watch src/index.tsbunx tsx --watch src/index.tsThe server listens on http://localhost:3000. In another terminal, send a craving and watch the recipe stream in field by field: title first, then description, then ingredients (with onSale: true on the ones the model picked from the tool), then steps.
curl -N -X POST http://localhost:3000/bargainChefFlow \ -H "Content-Type: application/json" \ -H "Accept: text/event-stream" \ -d '{"data":{"craving":"something warm with chicken"}}'The { "data": ... } wrapper is required: Genkit’s HTTP handler reads the flow input from the request body’s data field.
The response arrives as a series of data: events. Each event contains the partial recipe accumulated so far.
Test and inspect the app
Section titled “Test and inspect the app”You can call the flow directly with curl, and you can use the Developer UI to inspect every run alongside its tool calls and model invocations.
Send a request with curl
Section titled “Send a request with curl”With the server running, post a non-streaming request to get the final validated recipe in one shot:
curl -X POST http://localhost:3000/bargainChefFlow \ -H "Content-Type: application/json" \ -d '{"data":{"craving":"something warm with chicken"}}'You’ll receive a structured recipe as JSON, with ingredients flagged onSale when the model picked them from the tool.
Use the Developer UI
Section titled “Use the Developer UI”The Developer UI is Genkit’s local console for testing flows and inspecting execution traces. It runs alongside your backend code, gives you a visual runner for any flow in your project, and records every tool call and model invocation so you can iterate on prompts and debug tool behavior.
-
Start the Developer UI from your project root:
Terminal window npx genkit start -- tsx --watch src/index.tsTerminal window pnpm dlx genkit start -- tsx --watch src/index.tsTerminal window yarn dlx genkit start -- tsx --watch src/index.tsTerminal window bunx genkit start -- tsx --watch src/index.tsThis launches the Developer UI at
http://localhost:4000by default. -
Select
bargainChefFlowfrom the list of flows. -
Enter sample input:
{ "craving": "something warm with chicken" } -
Click Run.
You’ll see the generated recipe, with a trace that builds in real time so you can follow the flow’s progress through each tool call and model invocation.
What you built
Section titled “What you built”You now have a standalone Genkit backend on Fastify that streams structured output from Gemini over HTTP, calls a tool during generation to ground the model’s response in mock sale-price data, validates input and output against schemas, and surfaces every step in a local trace UI.
Next steps
Section titled “Next steps”- Creating flows: Compose multi-step flows, branch on input, and chain model calls.
- Generating content: Swap Gemini for another provider, tune sampling parameters, and work with multimodal input.
- Connect an app framework: Add a full-stack UI that calls your flow.
- Connect a web frontend: Wire a standalone web client up to this backend.
- Deploy your app: Ship to Cloud Run, Vercel, Firebase, or your own infrastructure.
- Developer tools: Dig deeper into the Developer UI, tracing, and evaluation.