Skip to content

Gin tutorial

In this tutorial, you’ll build Bargain Chef, a standalone Genkit backend on Gin 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.

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

Terminal window
mkdir my-genkit-gin && cd my-genkit-gin
go mod init example/my-genkit-gin

First, install the Genkit CLI:

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

Then install the Go packages you need:

Terminal window
go get github.com/firebase/genkit/go
go get github.com/firebase/genkit/go/plugins/googlegenai
go get github.com/gin-gonic/gin
go get github.com/gin-contrib/cors

These packages include:

  • github.com/firebase/genkit/go: Core Genkit Go SDK, including the Google AI plugin for Gemini.
  • github.com/firebase/genkit/go/plugins/googlegenai: Plugin that connects Genkit to Google’s Gemini models.
  • github.com/gin-gonic/gin: The Gin web framework.
  • github.com/gin-contrib/cors: CORS middleware so a browser-based frontend can call the backend.

This tutorial uses the Gemini API from Google AI Studio. Get a key at https://aistudio.google.com/apikey, then set the GEMINI_API_KEY environment variable to your key:

Terminal window
export GEMINI_API_KEY=<your API key>

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 Go structs so Genkit can validate the final output and stream partial recipe chunks.
  4. Define the streaming flow that ties everything together.

Create a main.go file:

main.go
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/plugins/googlegenai"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"google.golang.org/genai"
)
type SaleItem struct {
Name string `json:"name" jsonschema:"description=The ingredient name"`
Price string `json:"price" jsonschema:"description=The sale price, including units"`
}
type IngredientsOnSaleInput struct {
DayType string `json:"dayType" jsonschema:"enum=weekday,enum=weekend,description=Whether to fetch weekday or weekend sale prices"`
}
type RecipeIngredient struct {
Name string `json:"name" jsonschema:"description=Ingredient name"`
Quantity string `json:"quantity" jsonschema:"description=Amount needed (e.g. '2 cups', '1 lb')"`
OnSale bool `json:"onSale" jsonschema:"description=True if this ingredient is in the sale list"`
}
type Recipe struct {
Title string `json:"title" jsonschema:"description=Recipe title"`
Description string `json:"description" jsonschema:"description=Short description of the dish"`
Servings int `json:"servings" jsonschema:"description=Number of servings"`
Ingredients []RecipeIngredient `json:"ingredients" jsonschema:"description=The ingredient list"`
Steps []string `json:"steps" jsonschema:"description=The ordered preparation steps"`
}
type BargainChefInput struct {
Craving string `json:"craving" jsonschema:"description=What the user feels like eating right now"`
}
func main() {
ctx := context.Background()
g := genkit.Init(ctx,
genkit.WithPlugins(&googlegenai.GoogleAI{}),
)
getIngredientsOnSale := genkit.DefineTool(g, "getIngredientsOnSale",
"Returns the ingredients on sale at the local grocery store, with prices. The sale set differs between weekdays and weekends.",
func(toolCtx *ai.ToolContext, input IngredientsOnSaleInput) ([]SaleItem, error) {
// Mock data: in a real app, query a pricing database.
if input.DayType == "weekend" {
return []SaleItem{
{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"},
}, nil
}
return []SaleItem{
{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"},
}, nil
},
)
bargainChefFlow := genkit.DefineStreamingFlow(g, "bargainChefFlow",
func(ctx context.Context, input BargainChefInput, sendChunk func(context.Context, *Recipe) error) (*Recipe, error) {
today := time.Now().Weekday().String()
prompt := fmt.Sprintf(`Today is %s. The user is craving: %s.
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.`, today, input.Craving)
stream := genkit.GenerateDataStream[*Recipe](ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithConfig(&genai.GenerateContentConfig{
ThinkingConfig: &genai.ThinkingConfig{
ThinkingLevel: genai.ThinkingLevelMinimal,
},
}),
ai.WithTools(getIngredientsOnSale),
ai.WithPrompt(prompt),
)
for result, err := range stream {
if err != nil {
return nil, fmt.Errorf("failed to generate recipe: %w", err)
}
if result.Done {
return result.Output, nil
}
if result.Chunk != nil {
if err := sendChunk(ctx, result.Chunk); err != nil {
return nil, err
}
}
}
return nil, fmt.Errorf("stream ended without a final recipe")
},
)
r := gin.Default()
r.Use(cors.Default())
r.POST("/bargainChefFlow", gin.WrapH(genkit.Handler(bargainChefFlow)))
log.Println("Gin server listening on http://localhost:8080")
if err := r.Run(":8080"); err != nil {
log.Fatalf("server error: %v", err)
}
}

A few details are worth noting:

  • Final output and streamed chunks: genkit.GenerateDataStream[*Recipe] uses the Recipe struct both to validate the model’s final output and as the type for each streamed partial chunk, so fields can be absent in the in-progress chunks emitted during streaming.
  • The getIngredientsOnSale tool: The model decides when to call it based on the prompt, and the typed IngredientsOnSaleInput struct forces the model to pass dayType: "weekday" or "weekend". The jsonschema tags describe the schema to the model. In a real app, the tool would query a pricing database, inventory system, or third-party API.
  • sendChunk: genkit.DefineStreamingFlow takes a function that receives a sendChunk callback. The flow calls genkit.GenerateDataStream, which yields chunks as the model produces them, and forwards each one through sendChunk so the caller fills in field by field. When the stream is Done, the flow returns the final validated recipe.
  • The Gin handler: The server wraps the flow with gin.WrapH(genkit.Handler(bargainChefFlow)). genkit.Handler returns a standard http.Handler that parses the {"data": ...} envelope, validates input against the flow’s schema, runs the flow, and writes either a JSON response or a text/event-stream of chunks based on the request’s Accept header. gin.WrapH adapts that handler into a gin.HandlerFunc so it slots into Gin’s routing and middleware (including cors.Default()) like any other route. cors.Default() allows all origins, so any browser frontend can call this endpoint during development; before deploying, configure cors.New(...) with the origins you actually serve.

Verify that your project layout matches the structure below:

  • go.mod
  • go.sum
  • main.go

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 Gin server:

Terminal window
go run .

The server listens on http://localhost:8080 and exposes the flow at POST /bargainChefFlow. Leave it running while you test it from another terminal.

You can test the flow directly with curl, and you can use the Developer UI to inspect manual runs and any requests your app receives.

With the server running, call your flow over HTTP. Use the -N flag and an Accept: text/event-stream header to consume the streamed response:

Terminal window
curl -N -X POST http://localhost:8080/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, with fields such as title, ingredients, and steps filling in as the model generates them. The final event contains the complete, validated recipe.

To get a single non-streamed JSON response instead, omit the Accept header:

Terminal window
curl -X POST http://localhost:8080/bargainChefFlow \
-H "Content-Type: application/json" \
-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 your app under the Developer UI from your project root:

    Terminal window
    genkit start -- go run .

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