Skip to content

Django tutorial

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

The genkit-django package wires the flow into Django’s async (ASGI) stack with a single decorator, exposing both a JSON endpoint and a Server-Sent Events stream so any client can consume the partial recipe as it arrives, no manual request parsing or streaming code required.

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.11 or later
  • uv package manager

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

Create a new project with uv, install packages, then bootstrap Django:

Terminal window
mkdir my-genkit-django
cd my-genkit-django
uv init --no-readme --python 3.11
uv add django django-cors-headers uvicorn genkit genkit-django genkit-google-genai
uv run django-admin startproject myproject .
uv run python manage.py startapp recipes

These packages include:

  • django: The Django web framework.
  • django-cors-headers: CORS middleware. Lets browser frontends served from a different origin call the Genkit endpoint.
  • uvicorn: ASGI server that runs Django with async support.
  • genkit: Core Genkit SDK.
  • genkit-django: Helper that exposes Genkit flows as Django views, including server-sent events for streaming.
  • genkit-google-genai: Plugin that connects Genkit to Google’s Gemini models.

Install the Genkit CLI, which enables Genkit testing and observability:

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

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:

Terminal window
export GEMINI_API_KEY=<your API key>

Update myproject/settings.py so Django loads the recipes app and runs without the default database and admin middleware that Bargain Chef doesn’t need:

myproject/settings.py
SECRET_KEY = 'dev-only-change-me'
DEBUG = True
ALLOWED_HOSTS = ['*']
INSTALLED_APPS = [
'django.contrib.contenttypes',
'django.contrib.auth',
'corsheaders',
'recipes',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
]
# Allows all origins so any browser frontend can call the endpoint during
# development. Before deploying, set CORS_ALLOWED_ORIGINS to the origins you serve instead.
CORS_ALLOW_ALL_ORIGINS = True
ROOT_URLCONF = 'myproject.urls'
DATABASES = {}

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 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.

Create recipes/views.py:

recipes/views.py
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, Field
from genkit import FlowContext, Genkit
from genkit_django import genkit_django_handler
from genkit_google_genai import GeminiConfig, GoogleAI
ai = Genkit(
plugins=[GoogleAI()],
model=GoogleAI.gemini('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 = Field(serialization_alias='onSale')
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.')
@genkit_django_handler(ai)
@ai.flow(name='bargainChefFlow')
async def bargain_chef_flow(
input: BargainChefInput,
ctx: FlowContext[Recipe],
) -> 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=GeminiConfig(temperature=0.7, thinking_level='LOW'),
)
async for chunk in stream_response.stream:
if chunk.output:
ctx.send_chunk(chunk.output)
response = await stream_response.response
if response.output is None:
raise RuntimeError(f'Recipe generation failed: {response.error}')
return response.output

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

  • Final output and streamed chunks: output_schema=Recipe validates the final recipe returned at the end and parses partial Recipe objects as JSON tokens arrive. Annotating ctx: FlowContext[Recipe] types the streamed chunks sent through ctx.send_chunk. Return response.output (or response.text), never a raw ModelResponse, whose request field carries your system prompt, tools, and message history.
  • Shared Pydantic types: Recipe, RecipeIngredient, and BargainChefInput are Pydantic models validated by Genkit. serialization_alias='onSale' serializes on_sale as camelCase onSale over JSON and SSE while keeping snake_case in Python.
  • 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 the UI a typed view of the generated JSON as it grows. After the stream completes, the flow awaits stream_response.response so the HTTP request resolves with a validated recipe.
  • Failed generations: ai.generate_stream doesn’t raise when the model call fails, for example when Gemini is overloaded. The response comes back with output set to None and the cause in response.error. The flow raises with that cause so it shows up in your server log, and the client gets a generic Internal Error: a 500 response, or an error event when streaming. See Error types.
  • The @genkit_django_handler(ai) decorator: Stacked on top of @ai.flow, it adapts the flow into a csrf_exempt async Django view (requires ASGI, Django 5.0+) that parses {"data": ...} requests, runs the flow, and streams Server-Sent Events when the client sends Accept: text/event-stream or ?stream=true.
myproject/urls.py
from django.urls import path
from recipes.views import bargain_chef_flow
urlpatterns = [
path('bargainChefFlow', bargain_chef_flow),
]

The @genkit_django_handler decorator turns bargain_chef_flow into a Django view, so you can point a URL straight at it.

Pass context_provider= to @genkit_django_handler to extract caller identity before the flow runs. The provider receives a RequestData (from genkit.web import RequestData) with method, lowercased headers, input, and Django’s HttpRequest on request.

Raise PublicError from the provider or the flow to send a specific status and message, such as 401 for 'UNAUTHENTICATED'; Django’s PermissionDenied and Http404 in the provider also pass through. Any other exception becomes a 500 with a generic body. See Errors and timeouts for the full mapping.

Update the decorators on bargain_chef_flow in recipes/views.py:

from genkit import FlowContext, PublicError
from genkit.web import RequestData
from genkit_django import genkit_django_handler
# 1. Validate request headers and populate ctx.context
async def auth_context(req: RequestData) -> dict[str, object]:
auth_header = req.headers.get('authorization') or ''
token = auth_header.removeprefix('Bearer ').strip()
if not token:
raise PublicError('UNAUTHENTICATED', 'Missing bearer token')
return {'uid': token}
# 2. Read ctx.context inside the served flow
@genkit_django_handler(ai, context_provider=auth_context)
@ai.flow(name='bargainChefFlow')
async def bargain_chef_flow(
input: BargainChefInput,
ctx: FlowContext[Recipe],
) -> Recipe:
uid = ctx.context.get('uid')
...

Verify that your project layout matches the structure below:

  • pyproject.toml
  • manage.py
  • Directorymyproject
    • settings.py
    • urls.py
    • asgi.py
  • Directoryrecipes
    • views.py

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.

Start the Django app with Uvicorn so it can serve async requests and streaming responses:

Terminal window
uv run uvicorn myproject.asgi:application --reload

The server listens on http://localhost:8000.

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

Once the server is running, use the -N flag and an Accept: text/event-stream header to consume the streamed response:

Terminal window
curl -N -X POST http://localhost:8000/bargainChefFlow \
-H "Content-Type: application/json" \
-H "Authorization: Bearer user-123" \
-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 message event contains the partial recipe accumulated so far, with fields such as title, ingredients, and steps filling in as the model generates them. The last event carries the complete recipe under result.

To get a single non-streamed {"result": ...} JSON response instead, omit the Accept header:

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

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 myproject.asgi:application --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 Django 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.