Build a Cafe Management System

Build polyglot microservices in Go and Python with a Vue 3 frontend.

Introduction

30 Second Summary

Think about your favorite cafe. Behind the counter, one person takes your order, another makes your coffee, and someone else handles the register. They all work independently, but together they run the whole shop.

In this project, you will build a cafe management system using the same idea. You will create two backend services in different programming languages (Go and Python) that talk to each other over HTTP, wire them up to a Vue 3 frontend, and orchestrate everything with Docker Compose.

What You'll Build

A polyglot microservices system with three independently deployable services that work together to power a cafe management dashboard and an AI-powered chat assistant.

By the end of this project, you'll have:

  • A Go + Gin REST API that handles menu item CRUD operations with SQLite persistence.
  • A Python + FastAPI AI service that fetches live menu data from the Go service over HTTP and uses the Mistral AI API to power an intelligent cafe assistant.
  • A Vue 3 frontend styled with DaisyUI, featuring a staff menu management panel and a customer-facing AI chat interface.
  • A Docker Compose configuration that orchestrates all three services with a single command.
  • Secret Mission: integrate Stripe Checkout so customers can pay for cafe orders through a hosted payment page.

Are there any prerequisites?

You should have basic programming knowledge and be comfortable using the terminal. Familiarity with REST APIs is helpful. You will need Go, Python, Node.js, and Docker installed on your machine.

Set Up Your Development Environment

Before you can build microservices, you need each service's tools installed and ready to go.

This project has three independent services, each written in a different language. You will set up a Go project for the Menu Service, a Python project for the AI Service, and a Vue 3 project for the frontend.

In this step, get ready to:
  • Scaffold the Go Menu Service with Gin and GORM.
  • Scaffold the Python AI Service with FastAPI and the Mistral AI SDK.
  • Scaffold the Vue 3 frontend with Tailwind CSS and DaisyUI.
Create the project structure and install Go dependencies

Each microservice lives in its own folder, completely independent of the others. This separation is what makes microservices work. Each service can be built, tested, and deployed on its own.

  • Open your terminal.

How to open your terminal

Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.

Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create the project folder with subfolders for each service by running these commands:
mkdir cafe-microservices
cd cafe-microservices
mkdir menu-service ai-service frontend
  • Confirm the folders exist by running:
ls

You should see ai-service, frontend, and menu-service listed in the output.

  • Initialize the Go module for the Menu Service by running these commands:
cd menu-service
go mod init menu-service

What does go mod init do?

This creates a go.mod file that tracks all of the Go packages your service depends on. It is similar to a package.json in Node.js or a requirements.txt in Python.

  • Install Gin (the web framework), GORM (the ORM), and the SQLite driver by running:
go get -u github.com/gin-gonic/gin gorm.io/gorm gorm.io/driver/sqlite
  • Install the Gin CORS middleware package by running:
go get github.com/gin-contrib/cors

What are these packages?

  • Gin is a fast web framework for Go that handles HTTP routing, JSON parsing, and request validation.
  • GORM is a Go ORM that lets you interact with databases using Go structs instead of raw SQL.
  • The SQLite driver connects GORM to a SQLite database file stored locally.
  • The CORS middleware allows your frontend and other services to make requests to this API.

You should see output showing packages being downloaded. Go creates a go.sum file alongside your go.mod to lock dependency versions.

Seeing "go: command not found"?

Make sure Go is installed on your machine. Run go version to check. If it is not installed, download it from go.dev/dl.

Help me install Go on my machine.

Install Python dependencies and set up the AI Service

The AI Service is a separate Python application that will talk to both the Menu Service and the Mistral AI API. It needs its own isolated set of dependencies.

  • Navigate to the ai-service folder by running:
cd ../ai-service
  • Create a Python virtual environment by running:
python3 -m venv venv
  • Activate the virtual environment by running:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt.

What is a virtual environment?

A virtual environment is an isolated Python installation that keeps this project's packages separate from your system Python. This prevents version conflicts between different projects on your machine.

  • Install FastAPI, httpx, and the Mistral AI SDK by running:
pip install fastapi uvicorn[standard] httpx mistralai

What are these packages?

  • FastAPI is a modern Python web framework designed for building APIs with async support.
  • uvicorn is the server that runs your FastAPI application.
  • httpx is an async HTTP client that the AI Service will use to call the Go Menu Service over the network.
  • mistralai is the official Python SDK for the Mistral AI API.
  • Freeze your installed packages into a requirements.txt file by running:
pip freeze > requirements.txt

Why create a requirements.txt?

This file records every package and its exact version. When you build the Docker container later, Docker uses this file to install the same packages inside the container.

Now you need a Mistral AI API key so the AI Service can generate chat responses.

  • Open console.mistral.ai in your browser.
  • Sign up for a free account using your email, Google, or GitHub.
  • Verify your phone number when prompted.
  • Select the free Experiment plan in the billing section. No credit card is needed.
  • Navigate to the API Keys section in the sidebar.
  • Click Create new key.
  • Give it a name like cafe-project and confirm.
  • Copy the API key and save it somewhere safe. The key is only shown once.

Keep your API key private

Never share your API key publicly or commit it to version control. You will use it as an environment variable later when configuring Docker Compose.

Having trouble signing up for Mistral?

Make sure you select the Experiment plan (not a paid plan) in the billing section. Phone verification is required even for the free tier.

Help me set up my Mistral AI account and get an API key.

Scaffold the Vue 3 frontend with DaisyUI

The frontend is a Vue 3 single-page application that will consume both backend services. You will use Vite to scaffold it and DaisyUI for pre-built UI components like cards, tables, and chat bubbles.

  • Navigate back to the project root by running:
cd ..
  • Scaffold a new Vue 3 + TypeScript project inside the frontend folder by running:
npm create vite@latest frontend -- --template vue-ts

What does this command do?

This uses Vite's project scaffolding tool to create a Vue 3 project with TypeScript support. The --template vue-ts flag tells Vite to use the Vue + TypeScript template. Since the frontend folder already exists, Vite will scaffold the project files directly into it.

Vite may warn that the directory is not empty

If you see a prompt asking whether to remove existing files or ignore, select the option to ignore and continue. The folder is empty, so there is nothing to overwrite.

  • Move into the frontend folder and install the base dependencies by running:
cd frontend
npm install
  • Install Vue Router and Pinia for routing and state management by running:
npm install vue-router@4 pinia
  • Install Tailwind CSS, the Tailwind Vite plugin, and DaisyUI by running:
npm install tailwindcss@latest @tailwindcss/vite@latest daisyui@latest

You should see packages added successfully with no errors in the terminal output.

Seeing npm install errors?

Make sure Node.js is installed by running node --version. You need version 18 or higher. If you see peer dependency warnings, those are usually safe to ignore.

Help me fix my npm install errors.

Now you need to configure Vite and your CSS to use Tailwind and DaisyUI.

  • Open the cafe-microservices folder in your code editor.

How to open your project in VS Code

Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar. Type VS Code and press Enter to open it.

Click File in the top menu bar, then Open Folder, and select the cafe-microservices folder on your Desktop.

  • Open frontend/vite.config.ts in the Explorer sidebar.
  • Replace the entire file contents with the following code:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'

// Configure Vite with Vue and Tailwind CSS plugins
export default defineConfig({
  plugins: [
    vue(),
    tailwindcss(),
  ],
})

What does this configuration do?

This tells Vite to use two plugins. The vue() plugin enables Vue single-file component support. The tailwindcss() plugin integrates Tailwind CSS v4 directly into the Vite build pipeline.

  • Save vite.config.ts.
  • Open frontend/src/style.css in the Explorer sidebar.
  • Replace the entire file contents with the following two lines:
/* Import Tailwind CSS and DaisyUI component styles */
@import "tailwindcss";
@plugin "daisyui";

What do these lines do?

  • @import "tailwindcss" loads all of Tailwind CSS v4's utility classes.
  • @plugin "daisyui" registers DaisyUI as a Tailwind plugin, giving you access to component classes like btn, card, and chat-bubble.
  • Save style.css.

Verify that the frontend dev server starts correctly.

  • Back in your terminal, make sure you are inside the frontend folder.
  • Start the Vite dev server by running:
npm run dev

You should see output showing the dev server running at http://localhost:5173.

  • Open http://localhost:5173 in your browser.

You should see the default Vite + Vue welcome page.

  • Press Ctrl+C in your terminal to stop the dev server.

Dev server not starting?

Make sure you ran npm install before npm run dev. Also check that you are inside the frontend folder, not the project root.

Help me fix my Vite dev server.

Your development environment is fully set up. You have a Go project ready for the Menu Service, a Python project ready for the AI Service with a Mistral AI API key, and a Vue 3 frontend with Tailwind CSS and DaisyUI. Next up, you will build the Go Menu Service API with full CRUD operations.

Build the Go Menu Service API

In Step 1, you set up your development environment with Go, Python, and Vue tooling. Now it is time to build the first microservice.

The Menu Service is the backbone of your cafe system. It manages all menu items (drinks, pastries, snacks) and exposes a REST API that other services can call. You will build it in Go using the Gin web framework and GORM for database access.

In this step, get ready to:
  • Define the menu item data model and set up the SQLite database.
  • Build CRUD handlers for creating, reading, updating, and deleting menu items.
  • Wire up routes with CORS middleware and test the API with curl.
Define the menu item model and database setup

Every menu item needs a consistent shape: a name, description, category, price, and whether it is currently available. GORM lets you define this shape as a Go struct and automatically creates the database table from it.

  • Open the menu-service/ folder in your code editor.
  • Create a new file called main.go inside menu-service/ and add the following code:
package main

import (
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// MenuItem represents a single item on the cafe menu
type MenuItem struct {
	ID          uint    `json:"id" gorm:"primaryKey"`
	Name        string  `json:"name" binding:"required"`
	Description string  `json:"description"`
	Category    string  `json:"category" binding:"required"`
	Price       float64 `json:"price" binding:"required"`
	Available   *bool   `json:"available" gorm:"default:true"`
}

// db is the global database connection used by all handlers
var db *gorm.DB

// initDB opens the SQLite database and creates the menu_items table
func initDB() {
	var err error
	db, err = gorm.Open(sqlite.Open("menu.db"), &gorm.Config{})
	if err != nil {
		log.Fatal("Failed to connect to database:", err)
	}

	// AutoMigrate creates the table if it doesn't exist
	db.AutoMigrate(&MenuItem{})
}

// seedDB populates the database with sample cafe items
func seedDB() {
	var count int64
	db.Model(&MenuItem{}).Count(&count)

	// Only seed if the table is empty
	if count == 0 {
		available := true
		items := []MenuItem{
			{Name: "Espresso", Description: "Rich and bold single shot", Category: "Drinks", Price: 3.50, Available: &available},
			{Name: "Cappuccino", Description: "Espresso with steamed milk foam", Category: "Drinks", Price: 4.50, Available: &available},
			{Name: "Croissant", Description: "Buttery, flaky pastry", Category: "Pastries", Price: 3.00, Available: &available},
			{Name: "Blueberry Muffin", Description: "Freshly baked with real blueberries", Category: "Pastries", Price: 3.75, Available: &available},
		}
		db.Create(&items)
		log.Println("Database seeded with sample menu items")
	}
}

What does this code do?

  • The MenuItem struct defines the shape of a menu item. Each field has a json tag (for API responses), a gorm tag (for database behavior), and a binding tag (for request validation).
  • The Available field uses a pointer (*bool) so GORM can distinguish between "not provided" and "set to false."
  • The initDB function opens a SQLite file called menu.db and runs AutoMigrate to create the menu_items table automatically from the struct.
  • The seedDB function checks if the table is empty and inserts four sample items so you have data to work with immediately.
  • Save main.go.

You will not be able to run this file yet because it is missing a main() function. You will add that in the third substep after building the CRUD handlers.

Implement CRUD handlers

With the data model in place, you need handler functions that respond to HTTP requests. Each handler performs one operation: list all items, get one item, create, update, or delete.

  • Add the following handler functions to main.go, below the seedDB function. Start with the two read handlers:
// getMenuItems returns all menu items as JSON
func getMenuItems(c *gin.Context) {
	var items []MenuItem
	db.Find(&items)
	c.JSON(200, items)
}

// getMenuItem returns a single menu item by ID
func getMenuItem(c *gin.Context) {
	var item MenuItem
	if err := db.First(&item, c.Param("id")).Error; err != nil {
		c.JSON(404, gin.H{"error": "Menu item not found"})
		return
	}
	c.JSON(200, item)
}

What does this code do?

  • Each function takes a *gin.Context parameter, which gives you access to the request data and lets you write the response.
  • The db.Find(&items) call retrieves all rows from the menu_items table.
  • The c.Param("id") call extracts the :id path parameter from the URL (e.g., /api/menu/3).
  • If db.First cannot find a matching row, it returns a 404 error with a JSON message.
  • Below the read handlers, add the create, update, and delete handlers:
// createMenuItem adds a new menu item to the database
func createMenuItem(c *gin.Context) {
	var item MenuItem
	if err := c.ShouldBindJSON(&item); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}
	db.Create(&item)
	c.JSON(201, item)
}

// updateMenuItem modifies an existing menu item
func updateMenuItem(c *gin.Context) {
	var item MenuItem
	if err := db.First(&item, c.Param("id")).Error; err != nil {
		c.JSON(404, gin.H{"error": "Menu item not found"})
		return
	}
	if err := c.ShouldBindJSON(&item); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}
	db.Save(&item)
	c.JSON(200, item)
}

// deleteMenuItem removes a menu item from the database
func deleteMenuItem(c *gin.Context) {
	if err := db.Delete(&MenuItem{}, c.Param("id")).Error; err != nil {
		c.JSON(404, gin.H{"error": "Menu item not found"})
		return
	}
	c.JSON(200, gin.H{"message": "Menu item deleted"})
}

What does this code do?

  • The c.ShouldBindJSON(&item) call parses the request body into a MenuItem struct. If any field tagged binding:"required" is missing, Gin returns a 400 error automatically.
  • The db.Create, db.Save, and db.Delete are GORM methods that translate directly to SQL queries behind the scenes.
  • The update handler first checks that the item exists with db.First, then binds the new data and saves it.
  • You also need to add the Gin import at the top of main.go. Find the import block and update it to include Gin:
import (
	"log"

	"github.com/gin-gonic/gin"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)
  • Save main.go.

Seeing red squiggly lines in your editor?

Your editor might show import errors because you have not used the gin package in a runnable function yet. These errors will disappear once you add the main() function in the next substep.

Help me fix Go import errors in my menu-service main.go file.

Wire up routes and CORS

The handlers exist, but nothing connects them to actual URLs yet. You need a router that maps HTTP methods and paths to handler functions.

You also need CORS middleware so the Vue frontend and the Python AI Service can make requests to this API.

What is CORS?

Browsers block requests from one origin (like localhost:5173) to a different origin (like localhost:8080) by default. CORS (Cross-Origin Resource Sharing) is a set of HTTP headers that tells the browser which origins are allowed. Without it, your frontend cannot talk to your backend.

  • Add the following main() function at the bottom of main.go:
func main() {
	// Initialize database and seed sample data
	initDB()
	seedDB()

	// Create a Gin router with default middleware (logger and recovery)
	r := gin.Default()

	// Enable CORS for all origins so the frontend and AI service can connect
	r.Use(cors.Default())

	// Group all menu routes under /api/menu
	api := r.Group("/api/menu")
	{
		api.GET("", getMenuItems)          // GET /api/menu
		api.GET("/:id", getMenuItem)       // GET /api/menu/:id
		api.POST("", createMenuItem)       // POST /api/menu
		api.PUT("/:id", updateMenuItem)    // PUT /api/menu/:id
		api.DELETE("/:id", deleteMenuItem) // DELETE /api/menu/:id
	}

	// Start the server on port 8080
	log.Println("Menu Service running on :8080")
	r.Run(":8080")
}

What does this code do?

  • The gin.Default() call creates a router with built-in logging and crash recovery middleware.
  • The cors.Default() middleware allows all origins to make requests. This is fine for local development.
  • The r.Group("/api/menu") call creates a route group. Every route registered on api is automatically prefixed with /api/menu.
  • The r.Run(":8080") call starts the HTTP server on port 8080.
  • You also need to add the CORS import. Update the import block at the top of main.go to include the CORS package:
import (
	"log"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)
  • Save main.go.

Now run the Menu Service to make sure everything compiles and starts correctly.

  • Open your terminal and navigate to the menu-service folder by running:
cd menu-service
  • Start the server by running:
go run main.go

You should see Gin's startup output in your terminal, including the message Menu Service running on :8080 and a list of registered routes.

  • Open a second terminal window to test the API. List all menu items by running:
curl http://localhost:8080/api/menu

You should see a JSON array containing the four seeded menu items (Espresso, Cappuccino, Croissant, and Blueberry Muffin).

  • Test creating a new menu item by running:
curl -X POST http://localhost:8080/api/menu \
  -H "Content-Type: application/json" \
  -d '{"name": "Iced Latte", "description": "Cold espresso with milk", "category": "Drinks", "price": 5.00}'

You should see the newly created item returned as JSON with an assigned id field.

  • Test deleting a menu item by running:
curl -X DELETE http://localhost:8080/api/menu/5

You should see {"message":"Menu item deleted"} in the response.

Server not starting or curl returning errors?

  • If you see cannot find module providing package errors, make sure you ran go get for all dependencies in Step 1. Run go mod tidy to clean up and re-download missing packages.
  • If port 8080 is already in use, check for other running services. On macOS, run lsof -i :8080 to find the process.
  • If curl returns Connection refused, make sure the server is still running in your first terminal window.

Help me debug my Go Menu Service.

  • Stop the server by pressing Ctrl+C in the terminal where it is running.

Awesome, I've got everything!

Great. Double check you have saved main.go.

I'd like to double check the full code

Here is the complete main.go file for comparison:

package main

import (
	"log"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// MenuItem represents a single item on the cafe menu
type MenuItem struct {
	ID          uint    `json:"id" gorm:"primaryKey"`
	Name        string  `json:"name" binding:"required"`
	Description string  `json:"description"`
	Category    string  `json:"category" binding:"required"`
	Price       float64 `json:"price" binding:"required"`
	Available   *bool   `json:"available" gorm:"default:true"`
}

// db is the global database connection used by all handlers
var db *gorm.DB

// initDB opens the SQLite database and creates the menu_items table
func initDB() {
	var err error
	db, err = gorm.Open(sqlite.Open("menu.db"), &gorm.Config{})
	if err != nil {
		log.Fatal("Failed to connect to database:", err)
	}

	// AutoMigrate creates the table if it doesn't exist
	db.AutoMigrate(&MenuItem{})
}

// seedDB populates the database with sample cafe items
func seedDB() {
	var count int64
	db.Model(&MenuItem{}).Count(&count)

	// Only seed if the table is empty
	if count == 0 {
		available := true
		items := []MenuItem{
			{Name: "Espresso", Description: "Rich and bold single shot", Category: "Drinks", Price: 3.50, Available: &available},
			{Name: "Cappuccino", Description: "Espresso with steamed milk foam", Category: "Drinks", Price: 4.50, Available: &available},
			{Name: "Croissant", Description: "Buttery, flaky pastry", Category: "Pastries", Price: 3.00, Available: &available},
			{Name: "Blueberry Muffin", Description: "Freshly baked with real blueberries", Category: "Pastries", Price: 3.75, Available: &available},
		}
		db.Create(&items)
		log.Println("Database seeded with sample menu items")
	}
}

// getMenuItems returns all menu items as JSON
func getMenuItems(c *gin.Context) {
	var items []MenuItem
	db.Find(&items)
	c.JSON(200, items)
}

// getMenuItem returns a single menu item by ID
func getMenuItem(c *gin.Context) {
	var item MenuItem
	if err := db.First(&item, c.Param("id")).Error; err != nil {
		c.JSON(404, gin.H{"error": "Menu item not found"})
		return
	}
	c.JSON(200, item)
}

// createMenuItem adds a new menu item to the database
func createMenuItem(c *gin.Context) {
	var item MenuItem
	if err := c.ShouldBindJSON(&item); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}
	db.Create(&item)
	c.JSON(201, item)
}

// updateMenuItem modifies an existing menu item
func updateMenuItem(c *gin.Context) {
	var item MenuItem
	if err := db.First(&item, c.Param("id")).Error; err != nil {
		c.JSON(404, gin.H{"error": "Menu item not found"})
		return
	}
	if err := c.ShouldBindJSON(&item); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}
	db.Save(&item)
	c.JSON(200, item)
}

// deleteMenuItem removes a menu item from the database
func deleteMenuItem(c *gin.Context) {
	if err := db.Delete(&MenuItem{}, c.Param("id")).Error; err != nil {
		c.JSON(404, gin.H{"error": "Menu item not found"})
		return
	}
	c.JSON(200, gin.H{"message": "Menu item deleted"})
}

func main() {
	// Initialize database and seed sample data
	initDB()
	seedDB()

	// Create a Gin router with default middleware (logger and recovery)
	r := gin.Default()

	// Enable CORS for all origins so the frontend and AI service can connect
	r.Use(cors.Default())

	// Group all menu routes under /api/menu
	api := r.Group("/api/menu")
	{
		api.GET("", getMenuItems)          // GET /api/menu
		api.GET("/:id", getMenuItem)       // GET /api/menu/:id
		api.POST("", createMenuItem)       // POST /api/menu
		api.PUT("/:id", updateMenuItem)    // PUT /api/menu/:id
		api.DELETE("/:id", deleteMenuItem) // DELETE /api/menu/:id
	}

	// Start the server on port 8080
	log.Println("Menu Service running on :8080")
	r.Run(":8080")
}

Your Menu Service API is live and responding to requests. Next up, you will build the Python AI Service that calls this API to fetch live menu data and power an intelligent cafe assistant.

Build the FastAPI AI Service

Your Go Menu Service is live and serving menu data over a REST API. Now you need a second service that can consume that data and do something intelligent with it.

In this step, you will build the Python-based AI Service using FastAPI. This service calls the Menu Service over HTTP to fetch live menu data, then passes that data to the Mistral AI API to power a cafe assistant chatbot.

In this step, get ready to:
  • Build an inter-service HTTP client that fetches menu data from the Go service.
  • Integrate the Mistral AI API to create a menu-aware chat endpoint.
  • Add a health check endpoint and CORS middleware so the frontend can reach this service.
Create the inter-service client

The core pattern in a microservices system is one service calling another over the network. Your AI Service needs to fetch the current menu from the Go Menu Service before it can answer any questions about it.

You will use httpx, an async HTTP client for Python, to make this call.

  • Create a new file called main.py inside your ai-service folder.
  • Add the imports and inter-service client function by pasting this code:
import os
import httpx
from fastapi import FastAPI
from pydantic import BaseModel

# URL of the Go Menu Service (uses Docker service name in containers, localhost for local dev)
MENU_SERVICE_URL = os.getenv("MENU_SERVICE_URL", "http://localhost:8080")

async def get_menu_items():
    """Fetch all menu items from the Go Menu Service over HTTP."""
    try:
        async with httpx.AsyncClient() as client:
            # Call the Menu Service's list endpoint
            response = await client.get(f"{MENU_SERVICE_URL}/api/menu")
            response.raise_for_status()
            return response.json()
    except httpx.HTTPError as e:
        print(f"Error fetching menu: {e}")
        return []

What does this code do?

  • MENU_SERVICE_URL reads the Menu Service address from an environment variable. When running in Docker, this will be http://menu-service:8080 (the Docker service name). For local development, it defaults to http://localhost:8080.
  • get_menu_items() creates an async HTTP client, calls the Menu Service's /api/menu endpoint, and returns the JSON response as a Python list.
  • The try/except block catches connection errors gracefully. If the Menu Service is down, the function returns an empty list instead of crashing.

Why httpx instead of requests?

FastAPI is an async framework, so it works best with an async HTTP client. The httpx library provides an AsyncClient that integrates naturally with FastAPI's async/await pattern. The standard requests library is synchronous and would block the event loop.

  • Save main.py.

You won't see a change yet. The next chunk adds the Mistral AI integration that uses this function.

Integrate the Mistral AI API

With the inter-service client ready, you can now build the chat endpoint. This endpoint receives a user message, fetches the current menu from the Go service, and sends both to Mistral AI as context for an intelligent response.

  • In main.py, add the Mistral client setup and the Pydantic request model below your existing code:
from mistralai.client import Mistral

# Initialize the Mistral AI client with your API key
mistral_client = Mistral(api_key=os.getenv("MISTRAL_API_KEY", ""))

# Define the expected shape of incoming chat requests
class ChatRequest(BaseModel):
    message: str

What does this code do?

  • Mistral is imported from mistralai.client. This is the official Python SDK for the Mistral AI API (v2).
  • ChatRequest is a Pydantic model that validates incoming requests. FastAPI automatically rejects any request that does not include a message field.
  • Save main.py.

You won't see a change yet. The next chunk builds the actual chat endpoint.

  • Still in main.py, add the FastAPI app and chat endpoint below your ChatRequest model:
# Create the FastAPI application
app = FastAPI()

@app.post("/api/chat")
async def chat(request: ChatRequest):
    """Chat endpoint that uses menu data as context for AI responses."""
    # Fetch live menu data from the Go Menu Service
    menu_items = await get_menu_items()

    # Format menu items into a readable string for the AI
    menu_context = "\n".join(
        f"- {item['name']}: {item['description']} (${item['price']:.2f}, "
        f"{'available' if item.get('available', True) else 'unavailable'})"
        for item in menu_items
    ) if menu_items else "No menu items available right now."

    # Build the system prompt with live menu data
    system_prompt = (
        "You are a friendly and helpful cafe assistant. "
        "Here is the current menu:\n\n"
        f"{menu_context}\n\n"
        "Help customers with menu questions, recommendations, "
        "and order totals. Be concise and warm."
    )

    # Send the message to Mistral AI with menu context
    ai_response = await mistral_client.chat.complete_async(
        model="mistral-small-latest",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": request.message},
        ],
    )

    # Extract the AI's reply from the response
    reply = ai_response.choices[0].message.content

    return {"reply": reply}

What does this code do?

  • The /api/chat endpoint is where inter-service communication happens. It calls get_menu_items() to fetch live data from the Go service.
  • The menu data is formatted into a readable string and injected into the system prompt. This gives the AI full awareness of what is on the menu, including prices and availability.
  • complete_async() is the async variant of the Mistral SDK's chat method. Using the async variant prevents blocking FastAPI's event loop while waiting for the AI response.
  • The AI's reply is extracted from ai_response.choices[0].message.content and returned as JSON.
  • Save main.py.

You won't see a change yet. The next substep adds CORS and the health check so you can test the full service.

Seeing an import error for mistralai?

  • Make sure you activated your Python virtual environment before installing packages. Run source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows).
  • The import path changed in SDK v2. Make sure you are importing from mistralai.client, not from mistralai directly.
  • Help me fix my mistralai import error.
Add a health check and CORS

Before you can test the service, it needs two more things. A health check endpoint lets Docker Compose verify the service is running. CORS middleware allows the Vue frontend to make requests to this service from a different origin.

  • In main.py, add the CORS import at the top of the file with your other imports:
from fastapi.middleware.cors import CORSMiddleware
  • Add the CORS middleware and health route after the app = FastAPI() line, before the /api/chat endpoint:
# Allow requests from the Vue frontend and other services
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/api/health")
async def health_check():
    """Health check endpoint for Docker Compose."""
    return {"status": "healthy", "service": "ai-service"}

What does this code do?

  • CORSMiddleware with allow_origins=["*"] permits requests from any origin. This is fine for local development. In production, you would list specific allowed origins.
  • The /api/health endpoint returns a simple JSON object. Docker Compose can use this to confirm the service is up before routing traffic to it.

What is CORS?

CORS (Cross-Origin Resource Sharing) is a browser security feature. When your Vue frontend at http://localhost:80 tries to call your AI Service at http://localhost:8001, the browser blocks it unless the server explicitly allows it. The CORS middleware adds the necessary headers to permit these cross-origin requests.

  • Save main.py.

Awesome, I've got everything!

Double check you have saved main.py with all the code from the three chunks above.

I'd like to double check the full code

Here is the complete main.py file for your AI Service.

import os
import httpx
from fastapi import FastAPI
from pydantic import BaseModel
from fastapi.middleware.cors import CORSMiddleware
from mistralai.client import Mistral

# URL of the Go Menu Service (uses Docker service name in containers, localhost for local dev)
MENU_SERVICE_URL = os.getenv("MENU_SERVICE_URL", "http://localhost:8080")

async def get_menu_items():
    """Fetch all menu items from the Go Menu Service over HTTP."""
    try:
        async with httpx.AsyncClient() as client:
            # Call the Menu Service's list endpoint
            response = await client.get(f"{MENU_SERVICE_URL}/api/menu")
            response.raise_for_status()
            return response.json()
    except httpx.HTTPError as e:
        print(f"Error fetching menu: {e}")
        return []

# Initialize the Mistral AI client with your API key
mistral_client = Mistral(api_key=os.getenv("MISTRAL_API_KEY", ""))

# Define the expected shape of incoming chat requests
class ChatRequest(BaseModel):
    message: str

# Create the FastAPI application
app = FastAPI()

# Allow requests from the Vue frontend and other services
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/api/health")
async def health_check():
    """Health check endpoint for Docker Compose."""
    return {"status": "healthy", "service": "ai-service"}

@app.post("/api/chat")
async def chat(request: ChatRequest):
    """Chat endpoint that uses menu data as context for AI responses."""
    # Fetch live menu data from the Go Menu Service
    menu_items = await get_menu_items()

    # Format menu items into a readable string for the AI
    menu_context = "\n".join(
        f"- {item['name']}: {item['description']} (${item['price']:.2f}, "
        f"{'available' if item.get('available', True) else 'unavailable'})"
        for item in menu_items
    ) if menu_items else "No menu items available right now."

    # Build the system prompt with live menu data
    system_prompt = (
        "You are a friendly and helpful cafe assistant. "
        "Here is the current menu:\n\n"
        f"{menu_context}\n\n"
        "Help customers with menu questions, recommendations, "
        "and order totals. Be concise and warm."
    )

    # Send the message to Mistral AI with menu context
    ai_response = await mistral_client.chat.complete_async(
        model="mistral-small-latest",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": request.message},
        ],
    )

    # Extract the AI's reply from the response
    reply = ai_response.choices[0].message.content

    return {"reply": reply}

Now test the full AI Service. Make sure your Go Menu Service is still running on port 8080 from the previous step.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Navigate to your ai-service folder by running:
cd ~/Desktop/cafe-system/ai-service
  • Activate your virtual environment by running:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate
  • Set your Mistral API key as an environment variable. Replace your-api-key-here with the API key you grabbed from the Mistral console in Step 1.

macOS

export MISTRAL_API_KEY=your-api-key-here

Windows

$env:MISTRAL_API_KEY="your-api-key-here"
  • Start the AI Service by running:
uvicorn main:app --reload --port 8001

What does this command do?

  • uvicorn is the ASGI server that runs your FastAPI app.
  • main:app tells uvicorn to find the app object in main.py.
  • --reload automatically restarts the server when you save changes.
  • --port 8001 runs the service on port 8001, keeping it separate from the Menu Service on port 8080.

You should see output like Uvicorn running on http://127.0.0.1:8001 in your terminal.

Server not starting?

  • Make sure you are inside the ai-service folder, not the project root.
  • Check that your virtual environment is activated. You should see (venv) at the start of your terminal prompt.
  • If you see ModuleNotFoundError, install all dependencies by running pip install -r requirements.txt in your terminal.
  • Help me troubleshoot my FastAPI server startup.
  • Open another terminal window and test the health endpoint by running:
curl http://localhost:8001/api/health

You should see {"status":"healthy","service":"ai-service"} in the response.

  • Test the chat endpoint with a menu question by running:
curl -X POST http://localhost:8001/api/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "What coffee drinks do you have?"}'

You should see a JSON response with a reply field containing the AI assistant's answer about your cafe menu. The response references the actual menu items you seeded in the Go service.

Getting an empty menu or connection error?

  • Make sure your Go Menu Service is still running on port 8080 in a separate terminal. The AI Service calls it to fetch menu data.
  • If you see an authentication error from Mistral, double check that you set the MISTRAL_API_KEY environment variable correctly in the terminal where you started the AI Service.
  • If you restarted your terminal, you need to set the MISTRAL_API_KEY environment variable again. Environment variables set with export only last for the current terminal session.
  • Help me debug my AI chat endpoint.

Your two backend services are now talking to each other over HTTP. Next up, you will build the Vue 3 frontend that brings both services together into a single user interface.

Build the Vue 3 Frontend

Your two backend microservices are running and tested. The Go Menu Service handles CRUD operations for cafe items. The Python AI Service fetches menu data over HTTP and powers an intelligent assistant.

Now you need a frontend that brings both services together into a single interface. In this step, you will build a Vue 3 frontend with two panels: a staff dashboard for managing menu items and a customer-facing AI chat assistant.

In this step, get ready to:
  • Build a menu management panel with a Pinia store and DaisyUI table components.
  • Create an AI chat interface using DaisyUI chat bubble components.
  • Set up Vue Router for navigation and configure the Vite dev server proxy.
Create the menu store and management panel

The staff dashboard needs to display all menu items, let staff create new items, and edit existing ones. A Pinia store centralizes the menu data so any component can access it.

  • Create a new file called menuStore.ts inside the frontend/src/stores/ folder by running:
mkdir -p frontend/src/stores
touch frontend/src/stores/menuStore.ts
  • Confirm the file exists by running:
ls frontend/src/stores/

You should see menuStore.ts in the output.

  • Add the following code to menuStore.ts to define the menu item type and Pinia store:
import { defineStore } from 'pinia'
import { ref } from 'vue'

// Define the shape of a menu item from the Go service
export interface MenuItem {
  ID: number
  Name: string
  Description: string
  Category: string
  Price: number
  Available: boolean
}

export const useMenuStore = defineStore('menu', () => {
  // Reactive list of all menu items
  const items = ref<MenuItem[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)

  // Fetch all menu items from the Go Menu Service
  async function fetchItems() {
    loading.value = true
    error.value = null
    try {
      const response = await fetch('/api/menu')
      if (!response.ok) throw new Error('Failed to fetch menu items')
      items.value = await response.json()
    } catch (e: any) {
      error.value = e.message
    } finally {
      loading.value = false
    }
  }

  // Create a new menu item
  async function createItem(item: Omit<MenuItem, 'ID'>) {
    const response = await fetch('/api/menu', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(item),
    })
    if (!response.ok) throw new Error('Failed to create item')
    await fetchItems()
  }

  // Delete a menu item by ID
  async function deleteItem(id: number) {
    const response = await fetch(`/api/menu/${id}`, { method: 'DELETE' })
    if (!response.ok) throw new Error('Failed to delete item')
    await fetchItems()
  }

  return { items, loading, error, fetchItems, createItem, deleteItem }
})

What does this code do?

  • The MenuItem interface matches the JSON shape returned by the Go Menu Service. The field names are capitalized because Go exports them that way.
  • The useMenuStore function uses Pinia's Composition API syntax with ref() for reactive state.
  • Each function uses the native Fetch API to call the Go Menu Service endpoints. The /api/menu path will be proxied to the backend by Vite (configured later in this step).
  • After creating or deleting an item, the store re-fetches the full list to stay in sync.
  • Save menuStore.ts.

You will not see a change yet. The next chunk builds the Vue component that uses this store.

Now build the Vue component that renders the menu table and a form for adding new items.

  • Create a new file called MenuPanel.vue inside frontend/src/components/ by running:
mkdir -p frontend/src/components
touch frontend/src/components/MenuPanel.vue
  • Add the following template and script to MenuPanel.vue:
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useMenuStore } from '../stores/menuStore'

const store = useMenuStore()

// Form fields for creating a new menu item
const name = ref('')
const description = ref('')
const category = ref('Coffee')
const price = ref(0)

onMounted(() => {
  // Load menu items when the component first renders
  store.fetchItems()
})

async function handleCreate() {
  await store.createItem({
    Name: name.value,
    Description: description.value,
    Category: category.value,
    Price: price.value,
    Available: true,
  })
  // Reset form fields after creating
  name.value = ''
  description.value = ''
  price.value = 0
}
</script>

<template>
  <div class="p-6">
    <h2 class="text-2xl font-bold mb-4">Menu Management</h2>

    <!-- New item form using DaisyUI form controls -->
    <div class="card bg-base-200 p-4 mb-6">
      <h3 class="text-lg font-semibold mb-2">Add New Item</h3>
      <div class="flex flex-wrap gap-2">
        <input
          v-model="name"
          type="text"
          placeholder="Item name"
          class="input input-bordered w-48"
        />
        <input
          v-model="description"
          type="text"
          placeholder="Description"
          class="input input-bordered w-48"
        />
        <select v-model="category" class="select select-bordered">
          <option>Coffee</option>
          <option>Tea</option>
          <option>Pastry</option>
          <option>Sandwich</option>
        </select>
        <input
          v-model.number="price"
          type="number"
          step="0.01"
          placeholder="Price"
          class="input input-bordered w-24"
        />
        <button class="btn btn-primary" @click="handleCreate">Add</button>
      </div>
    </div>

    <!-- Loading spinner -->
    <div v-if="store.loading" class="flex justify-center p-8">
      <span class="loading loading-spinner loading-lg"></span>
    </div>

    <!-- Menu items table using DaisyUI table classes -->
    <div v-else class="overflow-x-auto">
      <table class="table">
        <thead>
          <tr>
            <th>Name</th>
            <th>Category</th>
            <th>Price</th>
            <th>Available</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="item in store.items" :key="item.ID">
            <td>
              <div class="font-bold">{{ item.Name }}</div>
              <div class="text-sm opacity-50">{{ item.Description }}</div>
            </td>
            <td><span class="badge badge-outline">{{ item.Category }}</span></td>
            <td>${{ item.Price.toFixed(2) }}</td>
            <td>
              <span :class="item.Available ? 'badge badge-success' : 'badge badge-error'">
                {{ item.Available ? 'Yes' : 'No' }}
              </span>
            </td>
            <td>
              <button class="btn btn-error btn-sm" @click="store.deleteItem(item.ID)">
                Delete
              </button>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

What does this component do?

  • The <script setup> block uses the Pinia store to fetch and manage menu items. The form fields are bound to local ref() values.
  • DaisyUI class names like input input-bordered, select select-bordered, btn btn-primary, and table handle all the styling with zero custom CSS.
  • The loading loading-spinner class shows a DaisyUI spinner while menu data loads from the backend.
  • Each table row renders a menu item with a badge for the category and a delete button.
  • Save MenuPanel.vue.

You will not see the component rendered yet. You will wire it up with Vue Router later in this step.

Seeing TypeScript errors in your editor?

Make sure you installed pinia in Step 1. If your editor shows red underlines on the Pinia import, try restarting the TypeScript language server.

help me fix TypeScript errors in my Vue frontend

Build the AI chat interface

The customer-facing side of the app is a chat interface where users can ask questions about the menu. Messages go to the Python AI Service, which fetches live menu data and responds using Mistral AI.

  • Create the chat store by running:
touch frontend/src/stores/chatStore.ts
  • Add the following code to chatStore.ts:
import { defineStore } from 'pinia'
import { ref } from 'vue'

// Each message has a role (user or assistant) and text content
export interface ChatMessage {
  role: 'user' | 'assistant'
  content: string
}

export const useChatStore = defineStore('chat', () => {
  const messages = ref<ChatMessage[]>([])
  const loading = ref(false)

  // Send a message to the AI Service and get a response
  async function sendMessage(userMessage: string) {
    // Add the user's message to the chat history
    messages.value.push({ role: 'user', content: userMessage })
    loading.value = true

    try {
      const response = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: userMessage }),
      })
      if (!response.ok) throw new Error('AI Service unavailable')
      const data = await response.json()
      // Add the AI's response to the chat history
      messages.value.push({ role: 'assistant', content: data.response })
    } catch {
      messages.value.push({
        role: 'assistant',
        content: 'Sorry, I could not reach the AI service. Please try again.',
      })
    } finally {
      loading.value = false
    }
  }

  return { messages, loading, sendMessage }
})

What does this code do?

  • The store holds a messages array that tracks the full conversation history between the user and the AI assistant.
  • The sendMessage function posts the user's text to /api/chat (the FastAPI AI Service endpoint). The Vite proxy routes this to port 8001.
  • If the AI Service is unreachable, a fallback error message appears in the chat instead of crashing the app.
  • Save chatStore.ts.

Now build the chat component that renders the conversation using DaisyUI chat bubble classes.

  • Create a new file called ChatPanel.vue inside frontend/src/components/ by running:
touch frontend/src/components/ChatPanel.vue
  • Add the following code to ChatPanel.vue:
<script setup lang="ts">
import { ref } from 'vue'
import { useChatStore } from '../stores/chatStore'

const store = useChatStore()
const input = ref('')

async function handleSend() {
  if (!input.value.trim()) return
  const message = input.value
  input.value = ''
  await store.sendMessage(message)
}
</script>

<template>
  <div class="flex flex-col h-full p-6">
    <h2 class="text-2xl font-bold mb-4">Cafe Assistant</h2>

    <!-- Scrollable message history -->
    <div class="flex-1 overflow-y-auto space-y-2 mb-4">
      <div v-for="(msg, i) in store.messages" :key="i">
        <!-- User messages appear on the right -->
        <div v-if="msg.role === 'user'" class="chat chat-end">
          <div class="chat-bubble chat-bubble-primary">{{ msg.content }}</div>
        </div>
        <!-- AI messages appear on the left -->
        <div v-else class="chat chat-start">
          <div class="chat-bubble">{{ msg.content }}</div>
        </div>
      </div>

      <!-- Loading indicator while waiting for AI response -->
      <div v-if="store.loading" class="chat chat-start">
        <div class="chat-bubble">
          <span class="loading loading-dots loading-sm"></span>
        </div>
      </div>
    </div>

    <!-- Message input area -->
    <div class="flex gap-2">
      <input
        v-model="input"
        type="text"
        placeholder="Ask about our menu..."
        class="input input-bordered flex-1"
        @keyup.enter="handleSend"
      />
      <button
        class="btn btn-primary"
        :disabled="store.loading"
        @click="handleSend"
      >
        Send
      </button>
    </div>
  </div>
</template>

What does this component do?

  • DaisyUI's chat chat-end and chat chat-start classes position messages on the right (user) and left (assistant) sides of the conversation.
  • The chat-bubble chat-bubble-primary class gives user messages a colored background to visually distinguish them from AI responses.
  • The loading loading-dots class shows an animated dots indicator inside a chat bubble while the AI is thinking.
  • Pressing Enter or clicking **Send** triggers handleSend(), which clears the input and calls the chat store.
  • Save ChatPanel.vue.

You will not see the chat component rendered yet. The next substep wires up routing and the Vite proxy.

Not sure if your chat store is correct?

Double-check that the sendMessage function posts to /api/chat with a JSON body containing a message field. This must match the FastAPI endpoint you built in Step 3.

help me debug my chat store

Set up routing and the Vite dev server proxy

With both panels built, you need Vue Router to switch between them. You also need to configure the Vite dev server to proxy API calls to the correct backend services.

  • Create a router configuration file by running:
mkdir -p frontend/src/router
touch frontend/src/router/index.ts
  • Add the following code to frontend/src/router/index.ts:
import { createRouter, createWebHistory } from 'vue-router'
import MenuPanel from '../components/MenuPanel.vue'
import ChatPanel from '../components/ChatPanel.vue'

// Define the two routes for the app
const routes = [
  { path: '/', redirect: '/menu' },
  { path: '/menu', component: MenuPanel },
  { path: '/chat', component: ChatPanel },
]

const router = createRouter({
  history: createWebHistory(),
  routes,
})

export default router

What does this code do?

  • The /menu route loads the staff dashboard. The /chat route loads the AI assistant.
  • The root path / redirects to /menu so the app always starts on the menu management page.
  • The createWebHistory() function enables clean URLs without hash fragments.
  • Save frontend/src/router/index.ts.

Now update the main app files to register the router, Pinia, and add a navigation bar.

  • Replace the contents of frontend/src/main.ts with the following:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './style.css'

// Create the Vue app and register Pinia + Router
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

What does this code do?

This is the entry point for the Vue app. It creates a Pinia instance for state management and registers the router for page navigation. Both plugins must be registered with app.use() before mounting.

  • Save main.ts.
  • Replace the contents of frontend/src/App.vue with the following:
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>

<template>
  <!-- DaisyUI navbar for switching between pages -->
  <div class="navbar bg-base-100 shadow-sm">
    <div class="flex-1">
      <a class="btn btn-ghost text-xl">Cafe Manager</a>
    </div>
    <div class="flex-none">
      <ul class="menu menu-horizontal px-1">
        <li><RouterLink to="/menu">Menu</RouterLink></li>
        <li><RouterLink to="/chat">Chat</RouterLink></li>
      </ul>
    </div>
  </div>

  <!-- Route content renders here -->
  <div class="container mx-auto" style="height: calc(100vh - 64px)">
    <RouterView />
  </div>
</template>

What does this component do?

  • The navbar class creates a DaisyUI navigation bar at the top of the page with the app title and two links.
  • The RouterLink components handle client-side navigation without full page reloads.
  • The RouterView component renders whichever panel matches the current URL.
  • Save App.vue.

The last piece is the Vite dev server proxy. During local development, your frontend runs on port 5173 but needs to reach the Go service on port 8080 and the Python service on port 8001.

  • Open frontend/vite.config.ts and replace its contents with the following:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [vue(), tailwindcss()],
  server: {
    proxy: {
      // Route menu API calls to the Go Menu Service
      '/api/menu': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
      // Route chat API calls to the Python AI Service
      '/api/chat': {
        target: 'http://localhost:8001',
        changeOrigin: true,
      },
      // Route health check calls to the Python AI Service
      '/api/health': {
        target: 'http://localhost:8001',
        changeOrigin: true,
      },
    },
  },
})

What does this proxy configuration do?

When you call fetch('/api/menu') from the browser, Vite intercepts the request and forwards it to http://localhost:8080/api/menu (the Go service). Calls to /api/chat go to http://localhost:8001/api/chat (the Python service). This avoids CORS issues during development because the browser thinks all requests go to the same origin.

  • Save vite.config.ts.
  • Start the Vite dev server to verify everything compiles by running:
cd frontend
npm run dev

You should see Vite start on http://localhost:5173. Open that URL in your browser.

You should see a navigation bar at the top with **Menu** and **Chat** links. The menu table will be empty (or show an error) because the Go backend is not running yet. That is expected.

Seeing a blank page or compile errors?

  • Check that main.ts imports ./style.css and that your style.css file contains the Tailwind and DaisyUI imports from Step 1.
  • Make sure vue-router and pinia are installed. Run npm ls vue-router pinia to confirm.
  • Verify that vite.config.ts includes both the vue() and tailwindcss() plugins.

help me fix my Vue 3 frontend build errors

Awesome, I've got everything!

Great. Double-check you have saved all files.

I'd like to double check the full code

Here is the complete vite.config.ts for reference:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [vue(), tailwindcss()],
  server: {
    proxy: {
      '/api/menu': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
      '/api/chat': {
        target: 'http://localhost:8001',
        changeOrigin: true,
      },
      '/api/health': {
        target: 'http://localhost:8001',
        changeOrigin: true,
      },
    },
  },
})

And here is the complete App.vue:

<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>

<template>
  <div class="navbar bg-base-100 shadow-sm">
    <div class="flex-1">
      <a class="btn btn-ghost text-xl">Cafe Manager</a>
    </div>
    <div class="flex-none">
      <ul class="menu menu-horizontal px-1">
        <li><RouterLink to="/menu">Menu</RouterLink></li>
        <li><RouterLink to="/chat">Chat</RouterLink></li>
      </ul>
    </div>
  </div>
  <div class="container mx-auto" style="height: calc(100vh - 64px)">
    <RouterView />
  </div>
</template>

Your frontend is ready. Next up, you will containerize all three services with Docker and orchestrate them with Docker Compose.

Containerize and Orchestrate with Docker Compose

Your three services work individually, but right now you need three separate terminal windows to run them all.

In this step, you'll package each service into its own Docker container and use Docker Compose to launch the entire system with a single command.

In this step, get ready to:
  • Write Dockerfiles for the Go Menu Service, Python AI Service, and Vue frontend.
  • Create a Docker Compose configuration that wires all three services together on a shared network.
  • Build and launch the full system, then test the end-to-end flow.
Write a Dockerfile for the Go Menu Service

A Dockerfile is a recipe that tells Docker how to build an image for your service. For the Go Menu Service, you'll use a multi-stage build: one stage compiles the Go binary, and a second, smaller stage runs it.

  • Create a file called Dockerfile inside your menu-service/ folder, then paste the following code:
# Stage 1: Build the Go binary
FROM golang:1.24 AS builder

WORKDIR /app

# Copy dependency files first for better Docker layer caching
COPY go.mod go.sum ./
RUN go mod download

# Copy the rest of the source code
COPY . .

# Build with CGO enabled (required for SQLite's C bindings)
RUN CGO_ENABLED=1 GOOS=linux go build -o menu-service .

# Stage 2: Create a slim runtime image
FROM debian:bookworm-slim

# Install the SQLite shared library needed at runtime
RUN apt-get update && apt-get install -y libsqlite3-0 && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copy only the compiled binary from the builder stage
COPY --from=builder /app/menu-service .

EXPOSE 8080

CMD ["./menu-service"]

What does this Dockerfile do?

  • Stage 1 uses the golang:1.24 image to compile your Go code into a single binary. CGO_ENABLED=1 is required because the SQLite driver uses C bindings under the hood.
  • Stage 2 uses a tiny debian:bookworm-slim image and copies only the compiled binary. This keeps the final image small because it doesn't include the Go compiler or source code.
  • The libsqlite3-0 package provides the SQLite shared library that the compiled binary needs at runtime.

Why a multi-stage build?

The Go compiler and all your source code are only needed during the build. A multi-stage build lets you throw away the build tools and ship just the tiny binary. This can shrink your image from over 1 GB to under 100 MB.

  • Save menu-service/Dockerfile.
Write a Dockerfile for the Python AI Service

The AI Service doesn't need a multi-stage build because Python is interpreted, not compiled. You'll use a slim Python image and install the dependencies from your requirements.txt.

  • Create a file called Dockerfile inside your ai-service/ folder, then paste the following code:
FROM python:3.13-slim

WORKDIR /app

# Copy and install dependencies first for better caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy the application code
COPY . .

EXPOSE 8001

# Start the FastAPI server with uvicorn
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]

What does this Dockerfile do?

  • python:3.13-slim is a lightweight Python image without extra development tools.
  • Copying requirements.txt before the rest of the code means Docker can cache the dependency layer. If you only change your Python code, Docker skips reinstalling packages.
  • --host 0.0.0.0 tells uvicorn to listen on all network interfaces inside the container, which is required for Docker networking to work.
  • Save ai-service/Dockerfile.
Write a Dockerfile for the Vue frontend

The Vue frontend needs a multi-stage build too. The first stage uses Node.js to compile your Vue app into static HTML, CSS, and JavaScript files. The second stage serves those files with nginx, a fast web server.

The nginx server also acts as a reverse proxy, forwarding API requests from the browser to the correct backend service inside the Docker network.

  • Create a file called nginx.conf inside your frontend/ folder, then paste the following configuration:
server {
    listen 80;

    # Serve the Vue app's static files
    location / {
        root /usr/share/nginx/html;
        index index.html;
        # Support Vue Router's history mode by falling back to index.html
        try_files $uri $uri/ /index.html;
    }

    # Proxy menu API requests to the Go Menu Service
    location /api/menu {
        proxy_pass http://menu-service:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # Proxy chat and health requests to the Python AI Service
    location /api/chat {
        proxy_pass http://ai-service:8001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /api/health {
        proxy_pass http://ai-service:8001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

What does this nginx config do?

  • The try_files directive supports Vue Router's history mode. When a user refreshes the page on a route like /chat, nginx serves index.html instead of returning a 404.
  • The proxy_pass directives forward API requests to the backend containers using their Docker service names (menu-service and ai-service). Docker's internal DNS resolves these names to the correct container IP addresses.
  • Save frontend/nginx.conf.
  • Now create a file called Dockerfile inside your frontend/ folder, then paste the following code:
# Stage 1: Build the Vue app
FROM node:22-alpine AS builder

WORKDIR /app

# Copy dependency files first for caching
COPY package.json package-lock.json ./
RUN npm install

# Copy the rest of the source code and build
COPY . .
RUN npm run build

# Stage 2: Serve the built files with nginx
FROM nginx:alpine

# Replace the default nginx config with our custom one
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Copy the built Vue app from the builder stage
COPY --from=builder /app/dist /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

What does this Dockerfile do?

  • Stage 1 uses node:22-alpine to install dependencies and run npm run build, which compiles your Vue app into static files in a dist/ folder.
  • Stage 2 uses nginx:alpine to serve those files. The custom nginx.conf replaces the default config so nginx knows how to proxy API requests to the backend services.
  • Save frontend/Dockerfile.

Seeing a build error for any Dockerfile?

  • Make sure each Dockerfile is named exactly Dockerfile with a capital D and no file extension.
  • Confirm each Dockerfile is in the correct service folder (menu-service/, ai-service/, or frontend/), not in the project root.
  • Still stuck? Help me debug my Dockerfile.
Create the Docker Compose configuration

Docker Compose lets you define all your services, networks, and environment variables in a single file. Instead of running three separate docker build and docker run commands, one docker compose up launches everything.

  • Create a file called docker-compose.yml in your project root folder (the parent folder that contains menu-service/, ai-service/, and frontend/), then paste the following configuration:
services:
  # Go Menu Service - handles menu CRUD operations
  menu-service:
    build: ./menu-service
    ports:
      - "8080:8080"
    networks:
      - cafe-network

  # Python AI Service - powers the cafe assistant chatbot
  ai-service:
    build: ./ai-service
    ports:
      - "8001:8001"
    environment:
      - MISTRAL_API_KEY=your-mistral-api-key-here
      - MENU_SERVICE_URL=http://menu-service:8080
    depends_on:
      - menu-service
    networks:
      - cafe-network

  # Vue frontend - staff dashboard and customer chat UI
  frontend:
    build: ./frontend
    ports:
      - "80:80"
    depends_on:
      - menu-service
      - ai-service
    networks:
      - cafe-network

networks:
  cafe-network:
    driver: bridge

What does this configuration do?

  • Each service has a build key pointing to the folder that contains its Dockerfile.
  • ports maps a port on your machine to a port inside the container. For example, "8080:8080" means localhost:8080 reaches the Menu Service container.
  • MENU_SERVICE_URL=http://menu-service:8080 is the key to inter-service communication. Inside the Docker network, services reach each other by name instead of localhost.
  • depends_on controls startup order. The AI Service waits for the Menu Service to start, and the frontend waits for both backends.
  • All three services share the cafe-network bridge network, which lets Docker's internal DNS resolve service names to container IPs.
  • Replace your-mistral-api-key-here with your actual Mistral API key from the Experiment plan.
  • Save docker-compose.yml.

What is service discovery?

When your services ran locally, the AI Service called the Menu Service at http://localhost:8080. Inside Docker, each service runs in its own container with its own network. Docker Compose creates a DNS entry for each service name, so http://menu-service:8080 resolves to the correct container automatically. This is called service discovery.

Not sure where to put docker-compose.yml?

Your project folder structure should look like this:

cafe-project/
  docker-compose.yml
  menu-service/
    Dockerfile
    main.go
    ...
  ai-service/
    Dockerfile
    main.py
    ...
  frontend/
    Dockerfile
    nginx.conf
    ...

The docker-compose.yml file sits at the same level as your three service folders.

Awesome, I've got everything!

Double check you've saved all four files: three Dockerfiles (one per service) and the docker-compose.yml in the project root.

I'd like to double check the full code

Here is the complete docker-compose.yml for reference:

services:
  # Go Menu Service - handles menu CRUD operations
  menu-service:
    build: ./menu-service
    ports:
      - "8080:8080"
    networks:
      - cafe-network

  # Python AI Service - powers the cafe assistant chatbot
  ai-service:
    build: ./ai-service
    ports:
      - "8001:8001"
    environment:
      - MISTRAL_API_KEY=your-mistral-api-key-here
      - MENU_SERVICE_URL=http://menu-service:8080
    depends_on:
      - menu-service
    networks:
      - cafe-network

  # Vue frontend - staff dashboard and customer chat UI
  frontend:
    build: ./frontend
    ports:
      - "80:80"
    depends_on:
      - menu-service
      - ai-service
    networks:
      - cafe-network

networks:
  cafe-network:
    driver: bridge
Build and launch the full system

Everything is wired up. Time to build all three Docker images and start the containers.

  • Make sure Docker Desktop is running. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Docker Desktop and press Enter to open it.
  • In your terminal, navigate to your project root folder (the one containing docker-compose.yml) by running:
cd ~/Desktop/cafe-project
  • Build and start all three services by running this command:
docker compose up --build

What does this command do?

  • docker compose up reads your docker-compose.yml and starts all defined services.
  • The --build flag forces Docker to rebuild the images from each Dockerfile before starting. Without it, Docker would reuse old images if they already exist.

The first build takes a few minutes because Docker needs to download base images and install all dependencies. You'll see logs from all three services streaming in your terminal.

Pro tip: watch for ready signals

Look for these lines in the terminal output to confirm each service started successfully:

  • Menu Service: Listening and serving HTTP on :8080
  • AI Service: Uvicorn running on http://0.0.0.0:8001
  • Frontend: the nginx container starts silently. If you see no errors from it, it's ready.

Now test each service to confirm the full system is working.

  • Open a new terminal tab or window.
  • Test the Menu Service by running this command:
curl http://localhost:8080/api/menu

You should see a JSON array of your seeded menu items (Espresso, Cappuccino, Croissant, etc.).

  • Test the AI Service health check by running:
curl http://localhost:8001/api/health

You should see a JSON response confirming the service is healthy.

  • Test the AI chat endpoint to verify inter-service communication by running:
curl -X POST http://localhost:8001/api/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "What drinks do you have?"}'

You should see an AI response that references actual menu items from the Go service. This proves the Python container is successfully calling the Go container over the Docker network.

  • Open your browser and navigate to http://localhost (port 80 is the default, so you don't need to type it).

You should see your Vue frontend with the DaisyUI navbar. Navigate to the menu page to see your menu items, then switch to the chat page and ask the AI assistant a question about the menu.

Services not starting or can't connect?

  • If you see port is already allocated, another process is using port 8080, 8001, or 80. Stop any locally running services from earlier steps before running Docker Compose.
  • If the AI Service logs show Connection refused when calling the Menu Service, the Menu Service might not be fully ready yet. Wait a few seconds and try the chat request again.
  • If the Go build fails with a CGO error, make sure your menu-service/Dockerfile includes CGO_ENABLED=1 in the build command.
  • If the frontend shows a blank page, check the browser console for errors. A common cause is a missing or incorrect nginx.conf file.
  • Still stuck? Help me debug my Docker Compose setup.
  • Test the full end-to-end flow: add a new menu item through the staff dashboard, then switch to the chat page and ask the AI about it.

The AI assistant should mention your newly added item. This confirms all three services are communicating correctly through the Docker network.

Your entire cafe management system is now running as a set of orchestrated containers. You've proven that your Go and Python services can discover each other by name, communicate over HTTP, and serve a unified frontend. Next up, the Secret Mission challenges you to add Stripe payments to your system.

Secret mission

Integrate Stripe Checkout for Cafe Payments

Your cafe system can manage menu items and chat about them. But what about actually paying for an order? In this secret mission, you will add Stripe Checkout to your microservices system so customers can select menu items and pay through a real hosted payment page. All in test mode, so no real money changes hands.

Before you go

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally using Docker, and both the Mistral AI Experiment plan and Stripe test mode are free. There are no ongoing costs.

Resources you used:

  • Docker containers for the Go Menu Service, Python AI Service, and Vue 3 frontend.
  • Docker images built during docker compose up --build.
  • SQLite database file created by the Menu Service.
  • Python virtual environment in the ai-service/ directory.
  • Node.js dependencies in the frontend/node_modules/ directory.
  • Mistral AI API key (free Experiment plan).
  • Stripe test mode API keys (secret mission only).

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with the cafe management system.

  • Your Docker containers will keep running until you stop them or restart your machine.
  • Your Mistral AI API key stays active on the free Experiment plan.
  • Your Stripe test mode keys remain available at no cost.

Pause - I'll come back to this later

Shut down running containers to free up system memory and CPU. Your project files and configuration stay intact so you can pick up where you left off.

  • Stop all running containers by running docker compose down from your project root directory.
  • Your project files, SQLite database, and API keys are all saved locally. Nothing is lost.
  • To resume later, run docker compose up --build from the project root to bring everything back up.

Delete - I don't want to use this again

Remove all project resources and start fresh. This frees up disk space used by Docker images and project files.

  • Stop and remove all containers, networks, and volumes by running docker compose down --volumes --rmi all from your project root directory.
  • Delete the entire project directory containing menu-service/, ai-service/, and frontend/.
  • Optionally, revoke your Mistral AI API key at console.mistral.ai under the API Keys section.
  • Optionally, delete your Stripe test API keys at dashboard.stripe.com/test/apikeys if you completed the secret mission.

Mission accomplished

That's a Wrap!

Congratulations! You just built a fully working polyglot microservices system from scratch. Two backend services written in different languages, a Vue 3 frontend consuming both, and Docker Compose orchestrating the whole thing with a single command.

You've learned how to:

  • Build a Go + Gin REST API microservice with SQLite persistence and full CRUD operations for managing cafe menu items.
  • Create a Python + FastAPI AI microservice that communicates with the Menu Service over HTTP using httpx and integrates the Mistral AI API to power a menu-aware cafe assistant.
  • Build a Vue 3 frontend with Pinia state management and DaisyUI styling that consumes multiple backend services through a staff dashboard and AI chat interface.
  • Containerize all three services with Docker multi-stage builds and orchestrate them with Docker Compose, bringing the entire system up with a single command.
  • Integrate Stripe Checkout in test mode to add a real payment flow to your cafe, letting customers order and pay through a hosted payment page.

Ready to quiz yourself?