Deploy an Event Analytics Microservice
Build a containerized full-stack app and deploy it to AWS ECS.
Introduction
30 Second Summary
Every time you buy a concert ticket or check sports game prices, there's a backend service crunching real-time pricing data and serving it to a dashboard. Companies like SeatGeek process millions of these data points through containerized microservices deployed to the cloud.
In this project, you will build your own event analytics microservice from scratch. You'll create a Python API that serves ticket pricing data, a React dashboard that visualizes it, containerize both with Docker, and deploy the backend to AWS.
What You'll Build
A full-stack containerized event analytics service with a FastAPI backend serving simulated ticket pricing data and a React/TypeScript dashboard displaying interactive pricing charts.
By the end of this project, you'll have:
- A Python FastAPI backend with endpoints for event data, pricing analytics, and health checks.
- A React/TypeScript dashboard built with Vite that fetches and visualizes pricing trends using interactive charts.
- Both services containerized with multi-stage Docker builds and orchestrated locally using Docker Compose.
- The backend deployed to AWS using Amazon ECR and ECS Express Mode with a live public URL.
- Secret Mission: Add a real-time ticket alert endpoint using WebSockets to push price-drop notifications to the dashboard.
Are there any prerequisites?
You should have about a year of experience with TypeScript and React basics. Familiarity with Python fundamentals is also helpful.
You'll need Node.js, Python, and Docker Desktop installed on your machine. You'll also set up an AWS Free Tier account in Step 1.
Before We Start
To deploy containers to the cloud, you need a connection between your local machine and AWS. That connection starts with an AWS account and a command-line tool that can talk to AWS services on your behalf.
In this step, you will create an AWS Free Tier account, install the AWS CLI, and confirm Docker Desktop is ready to build container images.
In this step, get ready to:
- Create an AWS Free Tier account and log in to the AWS Management Console.
- Install and configure AWS CLI v2 with IAM credentials.
- Verify Docker Desktop is running and can pull images.
Create an AWS Free Tier account
AWS offers a Free plan that gives you $100 in credits immediately, with up to $200 total. This is more than enough to cover this project.
- Navigate to https://aws.amazon.com/free/ in your browser.
- Click Create a free account.
- Select the Free account plan.
- Complete the sign-up with your email and a payment method. You will not be charged on the Free plan.
- After sign-up completes, log in to the AWS Management Console.
What is the AWS Free Tier?
AWS Free Tier gives new accounts $100 on sign-up plus up to $100 more by completing activities, totaling up to $200. The Free plan lasts 6 months or until credits are depleted.
For this project, you will use Amazon ECR and ECS Express Mode. Both fall well within Free Tier limits for the work you will do here.
🙋♀️ Can't find the Create a free account button?
Make sure you are on https://aws.amazon.com/free/ and not a different AWS page. The button should appear near the top of the page.
If you already have an AWS account, you can skip this substep and log in to your existing account.
Help me create my AWS account.
Install and configure AWS CLI v2
The AWS CLI is a command-line tool that lets you manage AWS services from your terminal. You need version 2 to use features like ECR authentication later in this project.
- Check if you already have AWS CLI v2 by running this command in your terminal:
aws --version
✔️ I see version 2.x or higher
Your output looks something like aws-cli/2.27.41 Python/3.11.6 ... with a version starting with 2. You are good to go. Skip to the configuration step below.
ⓧ I see version 1.x
You have an older version of the AWS CLI installed. You need to upgrade to version 2.
Uninstall version 1 first, then install version 2 using the instructions in the next tab. Refer to the official migration guide at AWS CLI v1-to-v2 migration for detailed uninstall steps for your platform.
After uninstalling v1, follow the install steps in the "Command not found" tab below.
ⓧ Command not found
AWS CLI is not installed yet. Install it now using the instructions for your operating system.
macOS
- Download and install the AWS CLI v2 package by running these commands:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg ./AWSCLIV2.pkg -target /
- You will be prompted for your macOS password because the installer needs admin permissions.
- After installation completes, close and reopen your terminal, then verify by running:
aws --version
You should see output starting with aws-cli/2. followed by a version number.
Windows
- Download and run the MSI installer by running this command in Command Prompt:
msiexec.exe /i https://awscli.amazonaws.com/AWSCLIV2.msi
- Follow the on-screen prompts to complete the installation.
- After installation completes, close and reopen your Command Prompt, then verify by running:
aws --version
You should see output starting with aws-cli/2. followed by a version number.
Still seeing 'command not found' after installing?
Close your terminal completely and open a fresh one. The installer adds the aws command to your PATH, but existing terminal sessions do not pick up the change automatically.
Help me fix my AWS CLI installation.
Create an IAM user and configure the CLI
You need an IAM user with programmatic access so the AWS CLI can make API calls on your behalf. You will create this user in the AWS Console and then feed its credentials to the CLI.
- In the AWS Console, search for IAM in the top search bar and select it.
- In the left navigation pane, click Users.
- Click Create user.
- Enter a user name (for example, event-analytics-admin) and click Next.
- Select Attach policies directly.
- Search for AdministratorAccess and check the box next to it.
- Click Next, then click Create user.
- Click on your new user name to open the user details page.
- Select the Security credentials tab.
- In the Access keys section, click Create access key.
- Select Command Line Interface (CLI) as your use case, check the confirmation box at the bottom, and click Next.
- Click Create access key.
- Copy your Access Key ID and Secret Access Key to a safe place. You will not be able to see the secret key again after closing this page.
Why AdministratorAccess?
For a learning project, AdministratorAccess simplifies setup by granting full permissions. In a production environment, you would use least-privilege policies scoped to only the services you need (ECR, ECS, etc.).
Now configure the CLI with your new credentials.
- Run the configuration command:
aws configure
- When prompted, enter your Access Key ID and press Enter.
- Enter your Secret Access Key and press Enter.
- For Default region name, enter us-east-1 and press Enter.
- For Default output format, enter json and press Enter.
- Verify your configuration works by running:
aws sts get-caller-identity
You should see a JSON response containing your account ID, user ARN, and user ID. This confirms the CLI can authenticate to AWS.
Seeing 'Unable to locate credentials'?
Make sure you completed aws configure and entered both your Access Key ID and Secret Access Key correctly. Run aws configure again to re-enter them.
Help me fix my AWS CLI credentials.
Verify Docker Desktop is running
Later in this project, you will build container images with Docker. Docker Desktop must be running for those commands to work.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Docker Desktop and press Enter to open it.
- Wait for the Docker whale icon to appear in your system tray or menu bar. This means Docker is ready.
- Confirm CLI access by running this command in your terminal:
docker --version
You should see a version number printed. Now run the hello-world test:
- Verify Docker can pull and run images by running:
docker run hello-world
You should see a message that starts with "Hello from Docker!" confirming your installation is working correctly.
Seeing 'Cannot connect to the Docker daemon'?
This means Docker Desktop is not running yet. Open Docker Desktop from your applications and wait for the whale icon to appear in your system tray before retrying the command.
Help me get Docker Desktop running.
Your environment is ready. You have an AWS account, a configured CLI, and Docker Desktop running. Next up, you will build the Python FastAPI backend that serves event analytics data.
Build the Python FastAPI Backend
Your cloud tools are ready. Now it is time to build the backend that powers the entire analytics service.
The backend is a Python REST API built with FastAPI that serves simulated event ticket pricing data. It exposes endpoints that the React dashboard will consume later to display pricing trends and demand metrics.
In this step, get ready to:
- Create the project directory structure with backend and frontend folders.
- Build a FastAPI application with endpoints for event data and pricing analytics.
- Test the API locally and explore its interactive documentation.
Create the project structure and Python API
Every microservice project starts with a clear folder structure. You will create a top-level project directory with separate folders for the backend and frontend services.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the project directory structure and move into the backend folder by running these commands:
mkdir -p event-analytics/backend event-analytics/frontend
cd event-analytics/backend
- Confirm the folders exist by running:
ls ../
You should see backend and frontend listed in the output.
Don't see both folders?
Make sure you used the -p flag with mkdir to create parent directories. If you accidentally created a flat structure, delete it and re-run the command.
Help me fix my folder structure.
- Create the requirements.txt file that lists the backend dependencies. Open Cursor and create a new file called requirements.txt inside the backend/ folder with this content:
fastapi==0.136.3
uvicorn==0.49.0
websockets==15.0
What are these dependencies?
FastAPI is the web framework for building the API. Uvicorn is an ASGI server that runs your FastAPI app. The websockets package provides WebSocket protocol support that uvicorn uses under the hood.
- Save requirements.txt.
Now you will build the data layer. The data.py module generates simulated ticket pricing history for events like concerts, sports, and theater shows.
- Create a new file called data.py inside the backend/ folder. Start by adding the imports and the pricing history generator function:
import random
from datetime import datetime, timedelta
def generate_pricing_history(base_price: float, days: int = 30) -> list[dict]:
history = []
current_price = base_price
start_date = datetime.now() - timedelta(days=days)
for i in range(days):
change = random.uniform(-0.05, 0.08) * base_price
current_price = max(base_price * 0.5, min(base_price * 2.0, current_price + change))
history.append({
"date": (start_date + timedelta(days=i)).strftime("%Y-%m-%d"),
"price": round(current_price, 2),
})
return history
What does this function do?
The generate_pricing_history function simulates how ticket prices fluctuate over 30 days. Each day, the price shifts by a random percentage of the base price (between -5% and +8%).
The max and min calls clamp the price to stay between 50% and 200% of the base price. This prevents unrealistic spikes or crashes.
- Below the function, add the event data and pre-generated pricing history:
EVENTS = [
{
"id": 1,
"name": "Taylor Swift - Eras Tour",
"venue": "MetLife Stadium",
"category": "concert",
"date": "2026-08-15",
"base_price": 250.00,
},
{
"id": 2,
"name": "Yankees vs Red Sox",
"venue": "Yankee Stadium",
"category": "sports",
"date": "2026-07-20",
"base_price": 85.00,
},
{
"id": 3,
"name": "Hamilton - Broadway",
"venue": "Richard Rodgers Theatre",
"category": "theater",
"date": "2026-09-01",
"base_price": 175.00,
},
{
"id": 4,
"name": "Knicks vs Lakers",
"venue": "Madison Square Garden",
"category": "sports",
"date": "2026-07-10",
"base_price": 120.00,
},
]
# Pre-generate pricing history for consistent data within a session
PRICING_DATA = {event["id"]: generate_pricing_history(event["base_price"]) for event in EVENTS}
What does this data represent?
The EVENTS list holds four sample events spanning concerts, sports, and theater. Each event has a base ticket price.
The PRICING_DATA dictionary pre-generates 30 days of pricing history for each event when the module loads. This keeps the data consistent within a single server session.
- Save data.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved data.py before moving on.
ⓧ I'd like to double check the full code
import random
from datetime import datetime, timedelta
def generate_pricing_history(base_price: float, days: int = 30) -> list[dict]:
history = []
current_price = base_price
start_date = datetime.now() - timedelta(days=days)
for i in range(days):
change = random.uniform(-0.05, 0.08) * base_price
current_price = max(base_price * 0.5, min(base_price * 2.0, current_price + change))
history.append({
"date": (start_date + timedelta(days=i)).strftime("%Y-%m-%d"),
"price": round(current_price, 2),
})
return history
EVENTS = [
{
"id": 1,
"name": "Taylor Swift - Eras Tour",
"venue": "MetLife Stadium",
"category": "concert",
"date": "2026-08-15",
"base_price": 250.00,
},
{
"id": 2,
"name": "Yankees vs Red Sox",
"venue": "Yankee Stadium",
"category": "sports",
"date": "2026-07-20",
"base_price": 85.00,
},
{
"id": 3,
"name": "Hamilton - Broadway",
"venue": "Richard Rodgers Theatre",
"category": "theater",
"date": "2026-09-01",
"base_price": 175.00,
},
{
"id": 4,
"name": "Knicks vs Lakers",
"venue": "Madison Square Garden",
"category": "sports",
"date": "2026-07-10",
"base_price": 120.00,
},
]
# Pre-generate pricing history for consistent data within a session
PRICING_DATA = {event["id"]: generate_pricing_history(event["base_price"]) for event in EVENTS}
Now you will build the main API application. The main.py file defines the FastAPI app, configures CORS, and exposes the endpoints that serve event data.
- Create a new file called main.py inside the backend/ folder. Start with the imports, app creation, CORS middleware, and health check endpoint:
import asyncio
import json
import random
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from data import EVENTS, PRICING_DATA
app = FastAPI(title="Event Analytics API", version="1.0.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/health")
def health_check():
return {"status": "healthy"}
What does this code do?
The FastAPI() call creates the application instance. The CORS middleware allows any origin to access the API, which is necessary for the frontend to fetch data from a different port during local development.
The /api/health endpoint is a simple health check that returns a JSON status. Container orchestrators like Docker and ECS use this to verify the service is alive.
- Below the health check, add the endpoint that lists all events with their pricing statistics:
@app.get("/api/events")
def list_events():
events_with_stats = []
for event in EVENTS:
pricing = PRICING_DATA[event["id"]]
prices = [p["price"] for p in pricing]
events_with_stats.append({
**event,
"current_price": prices[-1],
"min_price": min(prices),
"max_price": max(prices),
"avg_price": round(sum(prices) / len(prices), 2),
})
return {"events": events_with_stats}
What does this endpoint do?
The /api/events endpoint loops through each event and computes summary statistics from its pricing history: the current price (last day), minimum, maximum, and average.
The **event spread operator merges the base event data with the computed stats into a single dictionary.
- Next, add the endpoint that returns the full pricing history for a specific event:
@app.get("/api/events/{event_id}/pricing")
def get_event_pricing(event_id: int):
if event_id not in PRICING_DATA:
return {"error": "Event not found"}, 404
event = next((e for e in EVENTS if e["id"] == event_id), None)
return {
"event": event,
"pricing_history": PRICING_DATA[event_id],
}
What does this endpoint do?
The /api/events/{event_id}/pricing endpoint takes an event ID as a path parameter. It returns the event details along with the full 30-day pricing history array. The dashboard uses this data to draw the pricing chart.
- Finally, add the WebSocket endpoint that will power real-time price alerts:
@app.websocket("/ws/alerts")
async def websocket_alerts(websocket: WebSocket):
await websocket.accept()
try:
while True:
await asyncio.sleep(5)
event = random.choice(EVENTS)
drop_pct = random.uniform(5, 25)
alert = {
"type": "price_drop",
"event_name": event["name"],
"venue": event["venue"],
"drop_percentage": round(drop_pct, 1),
"message": f"Price dropped {round(drop_pct, 1)}% for {event['name']}!",
}
await websocket.send_text(json.dumps(alert))
except WebSocketDisconnect:
pass
What does this WebSocket endpoint do?
The /ws/alerts endpoint opens a persistent WebSocket connection. Every 5 seconds, it picks a random event and simulates a price-drop alert. You will connect the frontend to this endpoint in the Secret Mission.
- Save main.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py before moving on.
ⓧ I'd like to double check the full code
import asyncio
import json
import random
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from data import EVENTS, PRICING_DATA
app = FastAPI(title="Event Analytics API", version="1.0.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/health")
def health_check():
return {"status": "healthy"}
@app.get("/api/events")
def list_events():
events_with_stats = []
for event in EVENTS:
pricing = PRICING_DATA[event["id"]]
prices = [p["price"] for p in pricing]
events_with_stats.append({
**event,
"current_price": prices[-1],
"min_price": min(prices),
"max_price": max(prices),
"avg_price": round(sum(prices) / len(prices), 2),
})
return {"events": events_with_stats}
@app.get("/api/events/{event_id}/pricing")
def get_event_pricing(event_id: int):
if event_id not in PRICING_DATA:
return {"error": "Event not found"}, 404
event = next((e for e in EVENTS if e["id"] == event_id), None)
return {
"event": event,
"pricing_history": PRICING_DATA[event_id],
}
@app.websocket("/ws/alerts")
async def websocket_alerts(websocket: WebSocket):
await websocket.accept()
try:
while True:
await asyncio.sleep(5)
event = random.choice(EVENTS)
drop_pct = random.uniform(5, 25)
alert = {
"type": "price_drop",
"event_name": event["name"],
"venue": event["venue"],
"drop_percentage": round(drop_pct, 1),
"message": f"Price dropped {round(drop_pct, 1)}% for {event['name']}!",
}
await websocket.send_text(json.dumps(alert))
except WebSocketDisconnect:
pass
Test the API locally
The code is written. Before moving on, you need to install the dependencies and run the server to verify everything works.
- Make sure your terminal is inside the backend/ folder (event-analytics/backend on your Desktop).
- Create a Python virtual environment by running this command:
python -m venv venv
- Activate the virtual environment:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
Your terminal prompt now shows (venv) at the beginning, confirming the virtual environment is active.
What is a virtual environment?
A virtual environment is an isolated Python installation. Packages you install inside it stay separate from your system Python. This prevents dependency conflicts between projects.
- Install the project dependencies by running:
pip install -r requirements.txt
You should see output confirming that fastapi, uvicorn, and websockets were installed successfully.
Installation failed?
Make sure the (venv) prefix is visible in your prompt. If it is not, you need to activate the virtual environment first.
If you see a permissions error, confirm you are running inside the virtual environment and not your system Python.
Help me troubleshoot the installation error.
- Start the FastAPI development server by running:
uvicorn main:app --reload --port 8000
You should see output including a line that says Uvicorn is running on http://127.0.0.1:8000.
What does this command do?
The main:app argument tells uvicorn to import the app object from main.py. The --reload flag restarts the server when you change code. The --port 8000 flag sets the port explicitly.
- Open your browser and go to http://localhost:8000/docs.
You should see the FastAPI interactive Swagger UI with your three endpoints listed: /api/health, /api/events, and /api/events/{event_id}/pricing.
- Click on GET /api/events in the Swagger UI.
- Click Try it out, then click Execute.
You should see a JSON response containing an events array with four events, each including pricing statistics like current_price, min_price, max_price, and avg_price.
Seeing a connection error?
Make sure the uvicorn server is still running in your terminal. You should see log output showing requests being served.
If you see a ModuleNotFoundError in the terminal, confirm you activated the virtual environment before running uvicorn.
Help me debug my server connection.
💡 Why FastAPI over Flask?
FastAPI has built-in async support, automatic OpenAPI documentation (the Swagger UI you just tested), and native WebSocket support. These features align with SeatGeek's modern Python stack and make adding real-time features seamless without extra dependencies.
Your backend API is live and serving event data locally. Next up, you will build the React dashboard that consumes this data and visualizes it with charts.
Build the React/TypeScript Dashboard
Your backend API is serving event data at http://localhost:8000. Now you need a frontend that fetches this data and turns it into something visual.
In this step, you will scaffold a React/TypeScript application using Vite, then build a dashboard component that displays event pricing charts using Recharts.
In this step, get ready to:
- Scaffold a React/TypeScript project with Vite.
- Build a dashboard with an event selector, pricing stats, and a line chart.
- Configure the Vite dev server proxy to forward API requests to the backend.
Scaffold the React project with Vite
Vite is a modern build tool that scaffolds React projects with TypeScript support out of the box.
- Make sure you are in the event-analytics/ root directory, then scaffold the frontend by running this command:
npm create vite@latest frontend -- --template react-ts
What does this command do?
This uses the create-vite scaffolder to generate a React + TypeScript project inside the frontend/ directory. The -- separates npm arguments from the arguments passed to create-vite itself.
💡 Vite will ask you a question
The CLI will prompt you with "Install with npm and start now?" Select No. You will install dependencies manually in the next command so you can add Recharts at the same time.
- Move into the frontend/ directory and install the base dependencies by running these commands:
cd frontend
npm install
- Install the charting library by running:
npm install recharts
Why Recharts?
Recharts is a lightweight charting library built specifically for React. It wraps D3 internals into declarative React components like <LineChart> and <ResponsiveContainer>, so you get interactive charts without complex D3 setup.
- Start the Vite dev server to confirm everything works by running:
npm run dev
- You should see output showing the dev server is running at http://localhost:5173.
- Open http://localhost:5173 in your browser. You should see the default Vite + React welcome page.
- Press Ctrl+C in your terminal to stop the dev server for now.
Dev server not starting?
- Make sure you ran npm install before npm run dev. Without installed dependencies, Vite cannot start.
- Confirm you are inside the frontend/ directory (not the project root event-analytics/).
- If port 5173 is already in use, Vite will try the next available port and display it in the terminal output.
Help me troubleshoot my Vite dev server
Build the dashboard UI
The scaffolded project includes a default App.tsx with boilerplate content. You will replace it with a dashboard that fetches event data from the backend and renders a pricing chart.
- Open frontend/src/App.tsx in Cursor.
- Select all existing content and delete it.
- Add the imports and type definitions by pasting this code at the top of the file:
import { useEffect, useState } from "react";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
interface Event {
id: number;
name: string;
venue: string;
category: string;
date: string;
current_price: number;
min_price: number;
max_price: number;
avg_price: number;
}
interface PricingPoint {
date: string;
price: number;
}
What are these interfaces?
TypeScript interfaces define the shape of the data your component expects. Event matches the JSON structure returned by /api/events, and PricingPoint matches each item in the pricing history array from /api/events/{id}/pricing.
The Recharts imports bring in the chart components you will use to render the pricing line graph.
- Below the interfaces, add the App component that fetches data and renders the dashboard:
function App() {
const [events, setEvents] = useState<Event[]>([]);
const [selectedEventId, setSelectedEventId] = useState<number | null>(null);
const [pricingHistory, setPricingHistory] = useState<PricingPoint[]>([]);
useEffect(() => {
fetch("/api/events")
.then((res) => res.json())
.then((data) => {
setEvents(data.events);
if (data.events.length > 0) {
setSelectedEventId(data.events[0].id);
}
});
}, []);
useEffect(() => {
if (selectedEventId === null) return;
fetch(`/api/events/${selectedEventId}/pricing`)
.then((res) => res.json())
.then((data) => {
setPricingHistory(data.pricing_history);
});
}, [selectedEventId]);
const selectedEvent = events.find((e) => e.id === selectedEventId);
return (
<div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
<h1>Event Analytics Dashboard</h1>
<div style={{ marginBottom: "1rem" }}>
<label htmlFor="event-select" style={{ marginRight: "0.5rem" }}>
Select Event:
</label>
<select
id="event-select"
value={selectedEventId ?? ""}
onChange={(e) => setSelectedEventId(Number(e.target.value))}
>
{events.map((event) => (
<option key={event.id} value={event.id}>
{event.name}
</option>
))}
</select>
</div>
{selectedEvent && (
<div style={{ marginBottom: "1.5rem" }}>
<h2>{selectedEvent.name}</h2>
<p>
{selectedEvent.venue} | {selectedEvent.category} |{" "}
{selectedEvent.date}
</p>
<div style={{ display: "flex", gap: "2rem" }}>
<div>
<strong>Current:</strong> ${selectedEvent.current_price}
</div>
<div>
<strong>Min:</strong> ${selectedEvent.min_price}
</div>
<div>
<strong>Max:</strong> ${selectedEvent.max_price}
</div>
<div>
<strong>Avg:</strong> ${selectedEvent.avg_price}
</div>
</div>
</div>
)}
<div style={{ width: "100%", height: 400 }}>
<ResponsiveContainer>
<LineChart data={pricingHistory}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis domain={["auto", "auto"]} />
<Tooltip />
<Line
type="monotone"
dataKey="price"
stroke="#4f46e5"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
);
}
export default App;
What does this component do?
- The first useEffect fetches all events from /api/events on page load and auto-selects the first event.
- The second useEffect fetches the pricing history whenever the selected event changes.
- The dropdown lets the user pick an event. The stats row shows current, min, max, and average prices.
- The <LineChart> from Recharts renders the 30-day pricing history as an interactive line graph.
- Save App.tsx.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
import { useEffect, useState } from "react";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
interface Event {
id: number;
name: string;
venue: string;
category: string;
date: string;
current_price: number;
min_price: number;
max_price: number;
avg_price: number;
}
interface PricingPoint {
date: string;
price: number;
}
function App() {
const [events, setEvents] = useState<Event[]>([]);
const [selectedEventId, setSelectedEventId] = useState<number | null>(null);
const [pricingHistory, setPricingHistory] = useState<PricingPoint[]>([]);
useEffect(() => {
fetch("/api/events")
.then((res) => res.json())
.then((data) => {
setEvents(data.events);
if (data.events.length > 0) {
setSelectedEventId(data.events[0].id);
}
});
}, []);
useEffect(() => {
if (selectedEventId === null) return;
fetch(`/api/events/${selectedEventId}/pricing`)
.then((res) => res.json())
.then((data) => {
setPricingHistory(data.pricing_history);
});
}, [selectedEventId]);
const selectedEvent = events.find((e) => e.id === selectedEventId);
return (
<div style={{ padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
<h1>Event Analytics Dashboard</h1>
<div style={{ marginBottom: "1rem" }}>
<label htmlFor="event-select" style={{ marginRight: "0.5rem" }}>
Select Event:
</label>
<select
id="event-select"
value={selectedEventId ?? ""}
onChange={(e) => setSelectedEventId(Number(e.target.value))}
>
{events.map((event) => (
<option key={event.id} value={event.id}>
{event.name}
</option>
))}
</select>
</div>
{selectedEvent && (
<div style={{ marginBottom: "1.5rem" }}>
<h2>{selectedEvent.name}</h2>
<p>
{selectedEvent.venue} | {selectedEvent.category} |{" "}
{selectedEvent.date}
</p>
<div style={{ display: "flex", gap: "2rem" }}>
<div>
<strong>Current:</strong> ${selectedEvent.current_price}
</div>
<div>
<strong>Min:</strong> ${selectedEvent.min_price}
</div>
<div>
<strong>Max:</strong> ${selectedEvent.max_price}
</div>
<div>
<strong>Avg:</strong> ${selectedEvent.avg_price}
</div>
</div>
</div>
)}
<div style={{ width: "100%", height: 400 }}>
<ResponsiveContainer>
<LineChart data={pricingHistory}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis domain={["auto", "auto"]} />
<Tooltip />
<Line
type="monotone"
dataKey="price"
stroke="#4f46e5"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
);
}
export default App;
Now you need to tell Vite's dev server to forward any /api requests to your backend instead of trying to serve them as static files.
- Open frontend/vite.config.ts in Cursor.
- Replace the entire file contents with this configuration:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": {
target: "http://localhost:8000",
changeOrigin: true,
},
"/ws": {
target: "ws://localhost:8000",
ws: true,
},
},
},
});
What does the proxy do?
When your React app makes a request to /api/events, the Vite dev server intercepts it and forwards it to http://localhost:8000/api/events where your FastAPI backend is running.
The /ws proxy does the same for WebSocket connections. You will use this in the Secret Mission later.
- Save vite.config.ts.
Seeing a TypeScript error in vite.config.ts?
- Make sure you kept the import for @vitejs/plugin-react at the top. This plugin is included with the Vite scaffold and does not need a separate install.
- Check that the file uses double quotes consistently. Copy the code block above exactly as shown.
Help me fix my vite.config.ts
Now verify the full dashboard works end-to-end with the backend.
- Open a new terminal tab or window and navigate to the backend/ directory inside event-analytics/.
- Start the backend by running:
uvicorn main:app --reload --port 8000
- In a separate terminal tab, navigate to the frontend/ directory and start the frontend by running:
npm run dev
- Open http://localhost:5173 in your browser.
- You should see the "Event Analytics Dashboard" heading, a dropdown with event names, pricing stats (Current, Min, Max, Avg), and a line chart showing 30 days of pricing history.
- Select a different event from the dropdown. The chart and stats should update to show that event's data.
Chart not showing data?
- Make sure your backend is running in a separate terminal on port 8000. The frontend proxy forwards requests there.
- Open your browser's DevTools console (F12) and check for network errors. A 404 on /api/events means the proxy is not configured or the backend is not running.
- Confirm your virtual environment is activated in the backend terminal before running uvicorn.
Help me debug why my dashboard is not showing data
Your full-stack app is running locally. The frontend fetches real data from the backend and renders interactive charts. Next up, you will containerize both services using multi-stage Docker builds so they can run anywhere.
Dockerize with Multi-Stage Builds
Your backend and frontend both work locally, but they depend on specific runtimes, packages, and configuration on your machine. To deploy these services anywhere (a teammate's laptop, a cloud server), you need to package each one into a portable container that carries everything it needs.
In this step, you will write Dockerfiles for both services using a multi-stage build pattern. Multi-stage builds separate the heavy installation work from the final runtime image, producing containers that are small, fast, and secure.
In this step, get ready to:
- Write a multi-stage Dockerfile for the Python backend.
- Write a multi-stage Dockerfile for the React frontend (served by nginx).
- Orchestrate both containers locally with Docker Compose.
Create the backend Dockerfile
A Dockerfile is a recipe that tells Docker how to build a container image for your application. The multi-stage pattern uses two FROM statements: one to install dependencies (the builder), and another to run the app (the runtime).
- In Cursor, create a new file called Dockerfile inside your backend/ folder.
- Paste the following code into backend/Dockerfile:
# Stage 1: Install dependencies
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN python -m venv /opt/venv && \
/opt/venv/bin/pip install --no-cache-dir -r requirements.txt
# Stage 2: Runtime
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
COPY . .
ENV PATH="/opt/venv/bin:$PATH"
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
What does this Dockerfile do?
Stage 1 (builder) creates a virtual environment at /opt/venv and installs all Python packages there. This keeps installed packages isolated and easy to copy.
Stage 2 (runtime) starts from a fresh python:3.12-slim image. It copies only the finished virtual environment and your application code. The result is a smaller image because build tools and caches from Stage 1 are discarded.
💡 Why multi-stage builds?
A single-stage build leaves compilers, package caches, and source archives in the final image. Multi-stage builds discard everything except what the app needs at runtime. This is the same pattern SeatGeek uses in their production Docker pipelines for both frontend and backend services.
- Save backend/Dockerfile.
- Build the backend image by running this command in your terminal from the event-analytics/ project root:
docker build -t event-api ./backend
- You should see Docker pull the base image, install dependencies, and finish with a success message.
Build failing?
- Make sure Docker Desktop is running. The Docker daemon must be active for any build to work.
- Check that your file is named exactly Dockerfile (capital D, no file extension) inside the backend/ folder.
- Confirm requirements.txt exists in backend/ with your dependencies listed.
Why isn't my Docker build working for the backend?
✔️ Awesome, I've got everything!
Great. Make sure you have saved backend/Dockerfile before moving on.
ⓧ I'd like to double check the full code
# Stage 1: Install dependencies
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN python -m venv /opt/venv && \
/opt/venv/bin/pip install --no-cache-dir -r requirements.txt
# Stage 2: Runtime
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
COPY . .
ENV PATH="/opt/venv/bin:$PATH"
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Create the frontend Dockerfile and nginx config
The frontend needs a different approach. In production, you do not run a Vite dev server. Instead, you compile the React app into static files and serve them with nginx, a fast web server.
You also need nginx to proxy API requests to your backend container. This means you will create an nginx.conf file alongside the Dockerfile.
- Create a new file called nginx.conf inside your frontend/ folder.
- Paste the following configuration into frontend/nginx.conf:
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /ws/ {
proxy_pass http://api:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
What does this nginx config do?
- location / serves the built React static files. The try_files directive ensures client-side routing works by falling back to index.html.
- location /api/ forwards any API request to the backend container at http://api:8000. The hostname api is the service name defined in Docker Compose (coming next).
- location /ws/ handles WebSocket connections by upgrading the HTTP connection. This prepares the infrastructure for the Secret Mission.
- Save frontend/nginx.conf.
✔️ Awesome, I've got everything!
Great. Keep nginx.conf saved and continue to the Dockerfile.
ⓧ I'd like to double check the full code
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /ws/ {
proxy_pass http://api:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
- Create another new file called Dockerfile inside your frontend/ folder.
- Paste the following code into frontend/Dockerfile:
# Stage 1: Build the React app
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Serve with nginx
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
What does this Dockerfile do?
Stage 1 installs Node dependencies with npm ci (a clean, reproducible install) and runs npm run build to compile your React app into static HTML, CSS, and JavaScript in a dist/ folder.
Stage 2 starts from a tiny nginx:alpine image. It copies the built static files into nginx's serving directory and drops in your custom nginx.conf to handle API proxying. No Node.js runtime ends up in the final image.
- Save frontend/Dockerfile.
- Build the frontend image by running this command from your event-analytics/ project root:
docker build -t event-dashboard ./frontend
- You should see Docker install npm packages, run the build, and copy the output into the nginx image.
Frontend build failing?
- Make sure package-lock.json exists in frontend/. The npm ci command requires it. If it is missing, run npm install locally first to generate it.
- Check that nginx.conf is in the same frontend/ folder as the Dockerfile. The COPY nginx.conf instruction looks for it relative to the build context.
- Confirm your file is named Dockerfile (capital D, no extension) inside frontend/.
Why isn't my frontend Docker build succeeding?
✔️ Awesome, I've got everything!
Great. Make sure both frontend/Dockerfile and frontend/nginx.conf are saved.
ⓧ I'd like to double check the full code
# Stage 1: Build the React app
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Serve with nginx
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Create Docker Compose and test locally
You now have two separate container images. Docker Compose lets you define and run multiple containers together with a single command, wiring them into the same network so they can communicate by service name.
- Create a new file called docker-compose.yml in your event-analytics/ project root (the same level as backend/ and frontend/).
- Paste the following into docker-compose.yml:
services:
api:
build: ./backend
ports:
- "8000:8000"
dashboard:
build: ./frontend
ports:
- "80:80"
depends_on:
- api
What does this compose file do?
- api builds from ./backend and maps port 8000 on your machine to port 8000 in the container.
- dashboard builds from ./frontend and maps port 80 on your machine to port 80 in the container.
- depends_on ensures the api container starts before the dashboard container. Both containers share a Docker network, so the nginx config can reach the backend at http://api:8000.
- Save docker-compose.yml.
- Stop any locally running backend or frontend dev servers (they use the same ports).
- Start both containers by running this command from your event-analytics/ root:
docker compose up --build
- Wait for both containers to finish starting. You should see log output from both the api and dashboard services in your terminal.
- Open http://localhost in your browser.
- You should see your Event Analytics Dashboard with the pricing chart populated by live data from the API container.
Dashboard not loading at localhost?
- Check the terminal output for error messages. If the api service crashes, the dashboard cannot fetch data.
- Make sure nothing else is using port 80 or port 8000 on your machine. Stop any other dev servers or Docker containers that might occupy those ports.
- If you see a connection refused error in the browser, wait a few more seconds for both containers to fully start.
Help me debug why my Docker Compose containers aren't working together.
✔️ Awesome, I've got everything!
Great. Confirm you can see the pricing chart at http://localhost with data loading from the containerized API.
ⓧ I'd like to double check the full code
services:
api:
build: ./backend
ports:
- "8000:8000"
dashboard:
build: ./frontend
ports:
- "80:80"
depends_on:
- api
Your entire stack now runs in containers, isolated from your local machine's environment. Next up, you will push the backend image to AWS and deploy it to the cloud with a live public URL.
Deploy to AWS with ECR and ECS Express Mode
Your containers run perfectly on your machine. Both the API and dashboard start up, talk to each other, and serve real data through Docker Compose.
The real test is whether your backend works in the cloud, accessible to anyone on the internet. In this step, you'll push your backend image to Amazon ECR (a container registry managed by AWS) and deploy it as a live service using Amazon ECS Express Mode.
In this step, get ready to:
- Push your backend Docker image to Amazon ECR.
- Deploy the container with ECS Express Mode.
- Verify the API is live on the internet.
Push the backend image to Amazon ECR
Amazon ECR is a fully managed container registry. It stores your Docker images so that AWS services like ECS can pull and run them.
You'll create a repository, authenticate Docker to ECR, then tag and push your event-api image.
- First, retrieve your AWS account ID by running this command:
aws sts get-caller-identity --query Account --output text
What does this command do?
This queries your AWS account and returns just the 12-digit account ID as plain text. You'll need this ID for several commands in this step. Copy it somewhere handy.
- Create an ECR repository called event-api by running this command:
aws ecr create-repository --repository-name event-api --region us-east-1
What does this command do?
This creates a private repository named event-api in the us-east-1 region. The output includes a repositoryUri field showing the full address where your image will live.
- You should see JSON output containing a repositoryUri like 123456789012.dkr.ecr.us-east-1.amazonaws.com/event-api.
Seeing an access denied error?
Make sure your IAM user has the AdministratorAccess policy attached. You configured this in Step 1 when creating the IAM user.
Also confirm your AWS CLI is configured for the correct region by running aws configure list.
Help me troubleshoot my ECR access denied error.
- Authenticate Docker to your ECR registry by running this command:
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin [[AWS_ACCOUNT_ID="123456789012"]].dkr.ecr.us-east-1.amazonaws.com
What does this command do?
This pipes a temporary authentication token from AWS into docker login. It gives Docker permission to push images to your private ECR registry. The token is valid for 12 hours.
- You should see Login Succeeded in the terminal output.
- Tag your local event-api image with the ECR repository URI by running this command:
docker tag event-api:latest [[AWS_ACCOUNT_ID="123456789012"]].dkr.ecr.us-east-1.amazonaws.com/event-api:latest
Why do I need to tag the image?
Docker needs to know where to push the image. By tagging it with the full ECR URI, you're telling Docker that this image belongs in your ECR repository. The :latest tag identifies this as the most recent version.
- Push the tagged image to ECR by running this command:
docker push [[AWS_ACCOUNT_ID="123456789012"]].dkr.ecr.us-east-1.amazonaws.com/event-api:latest
- You should see Docker uploading each layer of the image. This may take a minute depending on your internet speed.
- When the push completes, you'll see a digest and size summary for the latest tag.
Push failing with authorization errors?
ECR login tokens expire after 12 hours. If you took a break between the login and push steps, re-run the aws ecr get-login-password command above to refresh your token.
Also double-check that your account ID in the tag command matches the one from aws sts get-caller-identity.
Help me fix my ECR push authorization error.
- Verify the image is in ECR by opening the AWS Management Console in your browser.
- Search for ECR in the top search bar and select Elastic Container Registry.
- Click on the event-api repository. You should see your image listed with the latest tag.
Deploy with ECS Express Mode
Your image is stored in the cloud. Now you need something to actually run it. ECS Express Mode handles all the infrastructure for you. It provisions a Fargate-based service, a load balancer, auto scaling, and a public URL.
- Open the Amazon ECS console by navigating to https://console.aws.amazon.com/ecs/v2 in your browser.
- In the left navigation pane, choose Express mode.
- Choose Create.
You'll see the "Let's set up your app" interface. This is where you tell ECS which container image to run.
- For Image URI, enter your ECR image URI:
[[AWS_ACCOUNT_ID="123456789012"]].dkr.ecr.us-east-1.amazonaws.com/event-api:latest
Use Browse ECR images
You can also choose Browse ECR images to select the event-api repository and its latest tag from a visual picker instead of typing the URI manually.
- For Task execution role, select Create new role from the dropdown.
- For Infrastructure role, select Create new role from the dropdown.
What are these roles?
The task execution role gives ECS permission to pull your container image from ECR and write logs. The infrastructure role gives ECS permission to manage networking and load balancer resources on your behalf.
By selecting Create new role, ECS creates these roles automatically with the correct AWS managed policies attached.
- Expand the Additional configurations - optional section.
- Under Container, find the Container port field.
- Change the port from 80 to 8000.
Why change the port?
Your FastAPI backend listens on port 8000 (as defined in the Dockerfile's CMD instruction). The default container port in Express Mode is 80, so you need to tell ECS which port your app actually uses.
- Choose Create at the bottom of the page.
ECS Express Mode begins provisioning your service. This includes a Fargate task, an Application Load Balancer, auto scaling policies, and a public URL. The process takes 2-5 minutes.
What is Fargate?
AWS Fargate is a serverless compute engine for containers. You don't need to manage EC2 instances. AWS handles the underlying servers. You just define your container, and Fargate runs it.
- Wait for the deployment to complete. You can monitor progress in the Resources tab.
- Once the service status shows the deployment is complete, look for the Application URL provided by ECS Express Mode.
- Copy this URL. It will look something like https://event-api-xxxxxxxx.ecs.us-east-1.on.aws.
Seeing an Invalid Parameter Exception error?
Some users see an error mentioning a service-linked role the first time they create an ECS service. This is a timing issue with role creation. Wait a few minutes and try choosing Create again.
Help me fix my ECS Express Mode deployment error.
Verify your live API
Your backend is now running in the cloud. Time to confirm it works by hitting the endpoints with the public URL ECS gave you.
- Test the health endpoint by opening this URL in your browser (replace with your actual URL):
[[ECS_URL="your-ecs-express-mode-url"]]/api/health
- You should see {"status": "healthy"} in your browser.
- Test the events endpoint by navigating to:
[[ECS_URL="your-ecs-express-mode-url"]]/api/events
- You should see your full event data returned as JSON, including pricing stats for Taylor Swift, Yankees vs Red Sox, Hamilton, and Knicks vs Lakers.
Your containerized backend is live on the internet. Anyone with the URL can access your event analytics API. This is the same Docker-to-AWS deployment pattern that production engineering teams use.
Getting a 503 or timeout error?
The service may still be starting up. ECS needs time to pull your image, start the container, and pass health checks. Wait 2-3 minutes and refresh.
If the error persists, go back to the ECS console, navigate to Clusters, select the Default cluster, and check the Services tab for error details.
Help me troubleshoot my ECS Express Mode deployment.
Secret mission
Add Real-Time Price Alerts with WebSockets
Your dashboard shows historical pricing data. But what if you could get notified the moment a price drops? In this extension, you'll add a WebSocket connection that pushes live price-drop alerts from the backend to your dashboard in real time.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Your AWS Free Tier credits cover the costs of this project, but leaving the ECS service running will consume credits over time.
Cost warning
Your ECS Express Mode service runs on Fargate, which bills per second of vCPU and memory usage. Your ECR repository charges $0.10/GB/month for storage (the free tier covers 500 MB for the first year). Both are covered by your $200 AWS Free Tier credits, but if you leave the ECS service running indefinitely, it will consume those credits faster.
If you are done with this project, choose Pause or Delete below to stop ongoing charges.
Resources you used:
- AWS ECS Express Mode service (runs your backend container on Fargate with a public URL).
- Amazon ECR repository event-api (stores your Docker images in us-east-1).
- Local event-analytics/ project folder on your Desktop (contains backend, frontend, and Docker Compose files).
- Local Docker images (event-api and event-dashboard built during the project).
Keep everything running
No action needed. Choose this if you are still actively building or want to show interviewers your live API endpoint.
- Your ECS Express Mode service stays live at its public URL.
- Monitor your credit usage in the AWS Billing console to avoid surprises.
- Your local Docker images and project files remain on disk.
Pause - I'll come back to this later
Shut down cloud resources to stop charges, but keep your local files and ECR image so you can redeploy later without rebuilding.
- Delete the ECS Express Mode service (see the Delete tab for console steps). This stops all Fargate charges.
- Keep the ECR repository. Your image stays stored for redeployment later. Storage costs are negligible under the free tier (500 MB included for one year).
- Keep your local event-analytics/ folder. You can redeploy anytime by re-pushing the image and creating a new ECS Express Mode service.
- Stop any running Docker Compose containers locally by pressing Ctrl+C in the terminal where docker compose up is running.
Delete - I don't want to use this again
Remove all project resources and start fresh. This stops all charges and frees local disk space.
- Delete the ECS Express Mode service from the AWS console:
- Open the ECS console at https://console.aws.amazon.com/ecs/v2.
- On the Clusters page, select your cluster.
- Choose the Services tab.
- Select your Express Mode service, then choose Delete.
- In the confirmation dialog, type delete and choose Delete to confirm.
Delete the ECR repository by running this command:
aws ecr delete-repository --repository-name event-api --region us-east-1 --force
- Stop and remove local Docker containers and images by running these commands:
docker compose down
docker rmi event-api event-dashboard
- Delete the local project folder:
macOS
cd ~/Desktop
rm -rf event-analytics
Windows
cd ~\Desktop
rmdir /s /q event-analytics
That's a Wrap!
That's a Wrap!
Nice work! You just built, containerized, and deployed a full-stack event analytics microservice using the same tools and patterns that power SeatGeek's production infrastructure.
You've learned how to:
- Build a Python FastAPI backend serving event analytics data with multiple REST endpoints and automatic Swagger documentation.
- Build a React/TypeScript dashboard with Vite and Recharts that fetches and visualizes live pricing data from an API.
- Containerize both services using multi-stage Docker builds and orchestrate them locally with Docker Compose.
- Deploy the backend to AWS using Amazon ECR and ECS Express Mode, making your API accessible via a live public URL.
- Secret Mission: Added WebSocket-based real-time price alerts connecting backend to frontend with a live notification feed.
Ready to quiz yourself?