Skip to content

FastAPI tutorial

In this tutorial, you’ll build Bargain Chef, a standalone Genkit backend on FastAPI that exposes a recipe-generating flow over HTTP. It uses two AI patterns Genkit simplifies: streaming structured output and tool calling.

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.

  • Python 3.10 or later
  • uv package manager

This tutorial assumes you’re already familiar with building FastAPI applications.

Terminal window
mkdir bargain-chef
cd bargain-chef
uv init --no-readme --python 3.10

Install the Genkit CLI:

Terminal window
curl -sL cli.genkit.dev | bash

Then install the packages you need in your project:

Terminal window
uv add fastapi uvicorn genkit genkit-google-genai genkit-fastapi

These packages include:

  • fastapi: The async Python web framework that serves your endpoint.
  • uvicorn: The ASGI server that runs your FastAPI app.
  • genkit: Core Genkit SDK.
  • genkit-google-genai: Plugin that connects Genkit to Google’s Gemini models.
  • genkit-fastapi: Mounts Genkit flows and agents on FastAPI with serve_flow / serve_agent, including streaming.

This tutorial uses the Gemini API from Google AI Studio. Get a key from Google AI Studio, then set the GEMINI_API_KEY environment variable to your key:

Terminal window
export GEMINI_API_KEY=<your API key>

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:

Terminal window
npx skills add genkit-ai/skills

See Develop with AI for tool-specific installation instructions.

The backend handles requests from clients. 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:

  1. Initialize Genkit and register Gemini as the model provider.
  2. Define a tool the model can call to fetch sale prices.
  3. Describe the recipe shape with Pydantic so Genkit can validate the final output and stream partial recipe chunks.
  4. Define the flow that ties everything together.

Replace the contents of main.py with the following:

main.py
from datetime import datetime
from typing import Literal
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from genkit import ActionRunContext, Genkit
from genkit_fastapi import serve_flow
from genkit_google_genai import GoogleAI
ai = Genkit(
plugins=[GoogleAI()],
model='googleai/gemini-flash-latest',
)
class SaleItem(BaseModel):
name: str
price: str
class GetIngredientsInput(BaseModel):
day_type: Literal['weekday', 'weekend'] = Field(
description='Whether to fetch weekday or weekend sale prices.',
)
@ai.tool()
async def get_ingredients_on_sale(input: GetIngredientsInput) -> list[SaleItem]:
"""Returns the ingredients on sale at the local grocery store, with prices.
The sale set differs between weekdays and weekends.
"""
# Mock data: in a real app, query a pricing database.
if input.day_type == 'weekend':
return [
SaleItem(name='chicken breast', price='$2.99/lb'),
SaleItem(name='pasta', price='$0.79'),
SaleItem(name='canned tomatoes', price='$0.99'),
SaleItem(name='garlic', price='$0.50 / head'),
SaleItem(name='olive oil', price='$6.99'),
]
return [
SaleItem(name='eggs', price='$3.49 / dozen'),
SaleItem(name='spinach', price='$1.99'),
SaleItem(name='parmesan', price='$4.99'),
SaleItem(name='lemons', price='$0.50 each'),
SaleItem(name='rice', price='$2.49'),
SaleItem(name='butter', price='$3.99'),
]
class RecipeIngredient(BaseModel):
name: str
quantity: str
on_sale: bool
class Recipe(BaseModel):
title: str
description: str
servings: int
ingredients: list[RecipeIngredient]
steps: list[str]
class BargainChefInput(BaseModel):
craving: str = Field(description='What the user feels like eating right now.')
@ai.flow(name='bargainChefFlow', chunk_type=Recipe)
async def bargain_chef_flow(input: BargainChefInput, ctx: ActionRunContext) -> Recipe:
today = datetime.now().strftime('%A')
stream_response = ai.generate_stream(
prompt=(
f'Today is {today}. The user is craving: {input.craving}.\n\n'
'Call the get_ingredients_on_sale tool with the day_type 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 on_sale=true if it appears in the tool's response, "
'false otherwise.'
),
tools=[get_ingredients_on_sale],
output_schema=Recipe,
config={'temperature': 0.7, 'thinkingConfig': {'thinkingLevel': 'MINIMAL'}},
)
async for chunk in stream_response.stream:
if chunk.output:
ctx.send_chunk(chunk.output)
response = await stream_response.response
if not response.output:
raise ValueError('Failed to generate recipe')
return response.output
app = FastAPI()
# allow_origins=['*'] lets any browser frontend call the endpoint during
# development. Before deploying, restrict it to the origins you actually serve.
app.add_middleware(
CORSMiddleware,
allow_origins=['*'],
allow_methods=['*'],
allow_headers=['*'],
)
# POST /bargainChefFlow — path defaults to the flow's name
app.include_router(serve_flow(bargain_chef_flow))

A few details are worth noting before you run the backend:

  • Final output and streamed chunks: Recipe is the complete recipe the flow returns at the end, passed as output_schema so Genkit validates the model’s output against it. Declaring chunk_type=Recipe on the flow tells Genkit that streamed chunks share the same shape, with fields filling in progressively as the model generates them.
  • Shared Python types: the Pydantic models (Recipe, RecipeIngredient, BargainChefInput) define the request and response shapes once, so the flow, the HTTP handler, and any client share a single source of truth.
  • The get_ingredients_on_sale tool: the model decides when to call it based on the prompt, and the typed GetIngredientsInput forces the model to pass day_type='weekday' or 'weekend'. In a real app, the tool would query a pricing database, inventory system, or third-party API.
  • ctx.send_chunk: each call pushes the latest partial recipe to the client, giving it a typed view of the generated JSON as it grows. After the stream completes, the flow awaits stream_response.response so the HTTP request still resolves with a validated recipe.

serve_flow returns a FastAPI APIRouter you mount with app.include_router. By default the route is POST /<flow name> (here /bargainChefFlow). When the client sends Accept: text/event-stream, the endpoint streams partial chunks as Server-Sent Events; otherwise it returns the final recipe as JSON. Pass base_path= or a router prefix= if you want a different URL.

Use context_dependency to wire FastAPI dependency injection into Genkit. The dependency returns a dict; Genkit makes that dict available on the flow as ctx.context—the same ActionRunContext you already use for send_chunk. Typical uses are auth, tenancy, and other values you already resolve with Depends:

from fastapi import Header
async def user_context(authorization: str = Header(...)) -> dict[str, object]:
# Validate the token and load the user in a real app.
return {'uid': authorization.removeprefix('Bearer ').strip()}
app.include_router(
serve_flow(bargain_chef_flow, context_dependency=user_context),
)

Inside the flow:

uid = (ctx.context or {}).get('uid')

For agents, use serve_agent the same way. One include_router call mounts the turn route. Configure a session store on the agent when you want server-managed sessions—only then does serve_agent also mount /getSnapshot and /abort. Without a store, those paths are not registered and return 404.

Pass the same context_dependency and Genkit applies it on every mounted route. Install genkit-google-cloud for the Firestore-backed store:

Terminal window
uv add genkit-google-cloud
from genkit_fastapi import serve_agent
from genkit_google_cloud import FirestoreSessionStore
bargain_chef_agent = ai.define_agent(
name='bargainChefAgent',
model='googleai/gemini-flash-latest',
system='You help users cook from grocery sale ingredients.',
tools=[get_ingredients_on_sale],
store=FirestoreSessionStore(),
)
app.include_router(
serve_agent(
bargain_chef_agent,
context_dependency=user_context,
),
prefix='/api',
)
# POST /api/bargainChefAgent
# POST /api/bargainChefAgent/getSnapshot
# POST /api/bargainChefAgent/abort

FirestoreSessionStore uses Application Default Credentials (or FIRESTORE_EMULATOR_HOST locally). See Session stores for options such as tenant prefixes, and Serve agents over HTTP for the turn envelope (data / init), streaming, and client connection details.

Verify that your project layout matches the structure below:

  • pyproject.toml
  • main.py

Start the FastAPI server:

Terminal window
uv run uvicorn main:app --reload

This launches the FastAPI app at http://localhost:8000. With the server running, send a streaming request from another terminal:

Terminal window
curl -N -X POST http://localhost:8000/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: title first, then description, then ingredients (with on_sale flags on the ones the model picked from the tool), then steps.

You can test the endpoint directly with curl, and you can use the Developer UI to inspect both manual runs and live requests.

For a non-streaming response, drop the Accept: text/event-stream header:

Terminal window
curl -X POST http://localhost:8000/bargainChefFlow \
-H "Content-Type: application/json" \
-d '{"data":{"craving":"something warm with chicken"}}'

You’ll receive the final structured recipe as a single JSON response.

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.

  1. Start the Developer UI from your project root:

    Terminal window
    genkit start -- uv run uvicorn main:app --reload

    This launches the Developer UI at http://localhost:4000 by default.

  2. Select bargainChefFlow from the list of flows.

  3. Enter sample input:

    { "craving": "something warm with chicken" }
  4. 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.

You now have a standalone Genkit backend on FastAPI 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.