Voice-Powered Strength Tracker API

Build and deploy a REST API with voice input using ASP.NET Core and Azure.

Introduction

30 Second Summary

Tracking your gym progress usually means scribbling weights on your phone's notepad or relying on memory. What if you could just say your workout out loud and have it recorded automatically?

In this project, you will build a voice-powered strength tracker API using C# and ASP.NET Core. You will speak your workout entries into the browser, log them to your API, and deploy the whole thing live on Azure.

What You'll Build

A REST API built with ASP.NET Core Minimal APIs that logs strength training sessions and serves a companion voice-input web page.

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

  • A working REST API with endpoints for logging workout sessions, filtering by exercise, and viewing your strength progress over time.
  • A voice-enabled web page that uses browser speech recognition to capture workout data hands-free.
  • A live deployment on Azure App Service where you can access your tracker from any device.
  • Secret Mission: Add a personal records endpoint that returns your all-time heaviest lift for each exercise.

Are there any prerequisites?

None. This project includes full setup instructions for every tool you need, including the .NET SDK, Azure CLI, and VS Code extensions.

Set Up Your Development Environment

Before you can build and deploy an API, you need the right tools on your machine. The .NET SDK gives you the compiler and runtime for C#. The Azure CLI lets you deploy to the cloud straight from your terminal.

In this step, you will install both tools, add C# support to VS Code, and create your free Azure account. Once everything is verified, you will be ready to start building your API.

In this step, get ready to:
  • Install the .NET 10 SDK and Azure CLI.
  • Set up VS Code with the C# Dev Kit extension.
  • Create an Azure free account and sign in from the terminal.
Install the .NET 10 SDK and Azure CLI

The .NET SDK is the toolkit that compiles and runs C# code. The Azure CLI is a command-line tool that lets you manage Azure cloud resources without leaving your terminal.

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal, and pressing Enter.
  • Install the .NET 10 SDK by running this command:
winget install Microsoft.DotNet.SDK.10

What does this command do?

This uses winget, the Windows package manager, to download and install the .NET 10 SDK on your machine. It handles the download, installer execution, and PATH configuration for you.

  • Once the installation finishes, close your terminal and reopen it so the new PATH takes effect.
  • Verify the installation by running:
dotnet --version

Check which output matches what you see:

✔️ I see a version starting with 10

Your .NET 10 SDK is installed and ready. The exact number (like 10.0.100 or 10.0.204) depends on the specific patch, but any version starting with 10. means you are good to go.

ⓧ I see an older version

You may have an older .NET SDK already on your machine that is taking priority on your PATH.

  • Close and reopen your terminal, then run dotnet --version again.
  • If it still shows an older version, run the install command above one more time and watch for errors in the output.
  • As a last resort, download the .NET 10 SDK installer directly from https://dotnet.microsoft.com/download and run it manually.

ⓧ Command not found

The install may not have completed, or your terminal session does not have the updated PATH.

  • Close your terminal completely and reopen it, then run dotnet --version again.
  • If it still fails, run the install command again:
winget install Microsoft.DotNet.SDK.10
  • Now install the Azure CLI by running:
winget install --exact --id Microsoft.AzureCLI

What does this command do?

This installs the Azure CLI using the exact package ID to avoid matching the wrong package. The --exact and --id flags ensure winget finds the correct Microsoft Azure CLI package.

  • Close and reopen your terminal again after the installation completes.
  • Verify the Azure CLI installation by running:
az --version

Check which output matches what you see:

✔️ I see a version number

Your Azure CLI is installed. You should see output that includes azure-cli followed by a version number like 2.87.0 or similar.

ⓧ Command not found

The install may not have completed, or your terminal session needs a refresh.

  • Close your terminal completely and reopen it, then run az --version again.
  • If it still fails, run the install command again:
winget install --exact --id Microsoft.AzureCLI
Install the C# Dev Kit extension in VS Code

The C# Dev Kit extension adds IntelliSense, debugging, and project management for C# inside VS Code. It auto-installs the core C# extension and the .NET Install Tool alongside it.

  • In VS Code, open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Type C# Dev Kit into the search bar.
  • Find the extension published by Microsoft (extension ID: ms-dotnettools.csdevkit).
  • Click Install.

What should I see?

After installation, you should see a green checkmark or an Uninstall button on the extension card. VS Code may also show a notification suggesting you reload the window.

If prompted to reload, click Reload to activate the extension.

Extension not showing up?

Make sure you search for C# Dev Kit (with spaces), not csdevkit. Look for the one published by Microsoft with a purple C# icon.

If the install button is greyed out, check that your VS Code is up to date by going to Help then Check for Updates.

help me install the C# Dev Kit extension in VS Code

Create an Azure free account and sign in

Azure App Service will host your finished API on the internet. The free tier costs nothing and gives you 60 CPU minutes per day, which is more than enough for this project.

  • Open https://azure.microsoft.com/free/ in your browser.
  • Click Start free and follow the prompts to create your account.
  • You will need a Microsoft account (or create one), a phone number for verification, and a credit card for identity verification (you will not be charged).

Why do I need a credit card?

Azure uses it for identity verification only. The Free F1 tier has hard spending limits, so your card will not be charged for this project.

  • Once your account is ready, return to your terminal and sign in by running:
az login
  • A browser window opens asking you to sign in with your Microsoft account. Complete the sign-in flow in the browser.
  • After signing in, your terminal displays your subscription details, including your subscription name and tenant ID.

Browser didn't open?

If the browser does not open automatically, look for a URL and a device code in your terminal output. Copy the URL into your browser manually and enter the code shown.

If you see a permissions error, make sure you are using the same Microsoft account you used to create the Azure free account.

help me sign in to Azure CLI

Your development environment is ready to go. Next up, you will create your API project and define the data model that tracks your workouts.

Create the API Project and Data Model

Your development environment is ready with the .NET SDK, Azure CLI, and VS Code all configured. Now it's time to create the actual project that powers your strength tracker.

Every REST API needs two things: a running server to handle requests, and a data model that defines what information it stores. You'll scaffold both in this step using ASP.NET Core Minimal APIs.

In this step, get ready to:
  • Scaffold an ASP.NET Core Minimal API project.
  • Define a workout session data model in C#.
  • Verify the project compiles and runs.
Scaffold a new ASP.NET Core project

The dotnet new web command generates a minimal API project with just two files: Program.cs (your application code) and a .csproj file (your project configuration).

  • In VS Code, open the integrated terminal by pressing Ctrl+` (backtick key, usually above Tab).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create the project by running this command:
dotnet new web -n StrengthTracker
  • You should see a message: The template "ASP.NET Core Empty" was created successfully..
  • Confirm the folder was created by running:
ls StrengthTracker
  • You should see Program.cs and StrengthTracker.csproj listed in the output.

Don't see the project files?

Make sure you ran the command from the Desktop directory. Run pwd to check your current location.

If you see a "command not found" error for dotnet, your .NET SDK installation from Step 1 may not have registered properly. Close and reopen your terminal, then try dotnet --version to verify the SDK is available.

help me troubleshoot why dotnet new web is not working

  • Now open the project in VS Code. Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the StrengthTracker folder, and click Open.

What did dotnet new web generate?

Program.cs contains your entire application. Right now it has just four lines that create a web server and add a single "Hello World!" route.

StrengthTracker.csproj tells .NET how to build your project. It targets net10.0 and enables features like nullable reference types.

Create the WorkoutSession data model

Your API needs a structure that defines what a workout session looks like. In C#, you define this as a class with typed properties.

  • In the Explorer sidebar, right-click in the empty space below the existing files.
  • Select New File.
  • Name the file WorkoutSession.cs and press Enter.
  • Paste the following code into WorkoutSession.cs:
public class WorkoutSession
{
    public int Id { get; set; }
    public string Exercise { get; set; } = string.Empty;
    public double WeightKg { get; set; }
    public int Reps { get; set; }
    public int Sets { get; set; }
    public DateTime Date { get; set; }
}

What does this code do?

This class defines the shape of a single workout session. Each property represents one piece of data the API stores:

  • Id uniquely identifies each session.
  • Exercise holds the name of the lift (like "bench press").
  • WeightKg, Reps, and Sets store the numbers for each session.
  • Date records when the session happened.
  • Save WorkoutSession.cs.
  • You should see WorkoutSession.cs appear in the Explorer sidebar alongside Program.cs.

Now you need a place to store workout sessions in memory. You'll add this storage to Program.cs.

  • Open Program.cs by clicking it in the Explorer sidebar.
  • Replace the entire contents of Program.cs with the following code:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// In-memory storage for workout sessions
var sessions = new List<WorkoutSession>();
var nextId = 1;

app.Run();

What does this code do?

This sets up a bare web server with in-memory data storage:

  • WebApplication.CreateBuilder(args) creates the ASP.NET Core web server with default settings.
  • sessions is a list that holds all logged workout sessions (it starts empty).
  • nextId tracks the next available ID to assign when a new session is created.
  • app.Run() starts the server and keeps it listening for requests.

You removed the "Hello World!" route from the scaffold because you'll add your own endpoints in the next step.

  • Save Program.cs.

Seeing red squiggly lines in Program.cs?

If WorkoutSession shows a red underline, the C# extension may still be loading. Wait a few seconds for the C# Dev Kit to finish analyzing your project.

If the error persists, make sure WorkoutSession.cs is saved and is in the same folder as Program.cs (the project root).

help me fix red underline errors in my ASP.NET Core project

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is what each file should look like at this point:

public class WorkoutSession
{
    public int Id { get; set; }
    public string Exercise { get; set; } = string.Empty;
    public double WeightKg { get; set; }
    public int Reps { get; set; }
    public int Sets { get; set; }
    public DateTime Date { get; set; }
}
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var sessions = new List<WorkoutSession>();
var nextId = 1;

app.Run();
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

</Project>
Run the project and confirm it responds

With the data model and server set up, you can now verify that everything compiles and runs correctly.

  • Open the integrated terminal in VS Code by pressing Ctrl+`.
  • Start the server by running this command:
dotnet run
  • You should see a "Now listening on" message in the terminal with a localhost URL (for example, http://localhost:5000).
  • Open that URL in your browser. You'll see a blank page, which is expected because you haven't added any endpoints yet.
  • Press Ctrl+C in the terminal to stop the server.

Server not starting?

If you see a build error mentioning WorkoutSession, make sure WorkoutSession.cs is saved and located in the same folder as Program.cs.

If the terminal says the port is already in use, try running dotnet run --urls http://localhost:5050 to use a different port.

help me troubleshoot why dotnet run is failing in my StrengthTracker project

Your project compiles and runs. Next up, you'll add the API endpoints that let you log workouts and retrieve your training history.

Build the API Endpoints

Your data model is ready and your project runs. Now it's time to build the endpoints that let you log workouts and retrieve your training history.

A REST API endpoint is a URL that your app listens to and responds to with data. You will create four endpoints: one to log sessions, one to retrieve all sessions, one to filter by exercise, and one to view progress over time.

In this step, get ready to:
  • Create a POST endpoint that logs new workout sessions with auto-assigned dates.
  • Create GET endpoints that retrieve sessions and show progress filtered by time period.
  • Test all endpoints using terminal commands to confirm they work.
Add the POST endpoint for logging sessions

The POST endpoint is the core of your tracker. It receives workout data from a request, auto-assigns an ID and the current date, then stores the session in your in-memory list.

  • In VS Code, switch back to Program.cs.
  • Find the line that says var app = builder.Build(); and add these two lines directly below it, above the var sessions line:
// Prepare to serve static HTML files from the wwwroot folder
app.UseDefaultFiles();
app.UseStaticFiles();
  • Now find the line var nextId = 1; and add the following POST endpoint between that line and app.Run();:
app.MapPost("/api/sessions", (WorkoutSession input) =>
{
    // Create a new session with auto-assigned ID and current date
    var session = new WorkoutSession
    {
        Id = nextId++,
        Exercise = input.Exercise,
        WeightKg = input.WeightKg,
        Reps = input.Reps,
        Sets = input.Sets,
        Date = DateTime.UtcNow
    };
    sessions.Add(session);
    return Results.Created($"/api/sessions/{session.Id}", session);
});

What does this code do?

The UseDefaultFiles() and UseStaticFiles() lines prepare your app to serve HTML files from a wwwroot folder. You will create that folder in the next step.

The MapPost method registers a handler at /api/sessions that receives a WorkoutSession object from the request body. It auto-assigns an ID using nextId++ and stamps the current UTC time as the date. After storing the session, it returns a 201 Created response with the saved data.

  • Save Program.cs.

Why auto-assign the date?

By using DateTime.UtcNow on the server, every session gets an accurate timestamp. This is more reliable than expecting the client to send a date, which could be wrong or missing.

Add GET endpoints for retrieving sessions

Your API needs ways to read data back. You will add three GET endpoints: one for all sessions, one filtered by exercise name, and one for viewing progress over a time period.

  • In Program.cs, add the following two GET endpoints directly below the MapPost block you just wrote (still above app.Run();):
app.MapGet("/api/sessions", () => sessions);

app.MapGet("/api/sessions/{exercise}", (string exercise) =>
{
    // Filter sessions by exercise name, ignoring upper/lowercase differences
    var filtered = sessions
        .Where(s => s.Exercise.Equals(exercise, StringComparison.OrdinalIgnoreCase))
        .ToList();
    return Results.Ok(filtered);
});

What does this code do?

The first MapGet at /api/sessions returns the entire sessions list. Simple and useful for seeing all your logged workouts.

The second MapGet at /api/sessions/{exercise} uses a route parameter. When someone visits /api/sessions/squat, it filters and returns only squat sessions. The StringComparison.OrdinalIgnoreCase ensures "Squat" and "squat" both match.

  • Save Program.cs.
  • Now add the progress endpoint directly below the two GET endpoints you just added (still above app.Run();):
app.MapGet("/api/progress", (string? period) =>
{
    var now = DateTime.UtcNow;
    // Determine how far back to look based on the period query string
    var cutoff = period?.ToLower() switch
    {
        "week" => now.AddDays(-7),
        "month" => now.AddMonths(-1),
        "6months" => now.AddMonths(-6),
        "year" => now.AddYears(-1),
        _ => DateTime.MinValue
    };

    // Filter by date, group by exercise, and return ordered results
    var filtered = sessions
        .Where(s => s.Date >= cutoff)
        .GroupBy(s => s.Exercise)
        .Select(g => new
        {
            Exercise = g.Key,
            Sessions = g.OrderBy(s => s.Date).Select(s => new
            {
                s.Date,
                s.WeightKg,
                s.Reps,
                s.Sets
            })
        });

    return Results.Ok(filtered);
});

What does this code do?

This endpoint accepts an optional period query string (like ?period=week). The switch expression maps each period value to a cutoff date. If no period is given, it defaults to DateTime.MinValue which means "show all time."

The LINQ chain filters sessions by the cutoff date, groups them by exercise, then returns each group ordered by date. This lets you see your progress for each exercise over your chosen time window.

  • Save Program.cs.

Seeing red squiggles in VS Code?

If you see errors under Where or GroupBy, make sure your StrengthTracker.csproj has <ImplicitUsings>enable</ImplicitUsings>. This automatically imports the System.Linq namespace for you.

help me fix LINQ errors in my ASP.NET Core project

✔️ Awesome, I've got everything!

Great. Double check you have saved Program.cs.

ⓧ I'd like to double check the full code

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseDefaultFiles();
app.UseStaticFiles();

var sessions = new List<WorkoutSession>();
var nextId = 1;

app.MapPost("/api/sessions", (WorkoutSession input) =>
{
    var session = new WorkoutSession
    {
        Id = nextId++,
        Exercise = input.Exercise,
        WeightKg = input.WeightKg,
        Reps = input.Reps,
        Sets = input.Sets,
        Date = DateTime.UtcNow
    };
    sessions.Add(session);
    return Results.Created($"/api/sessions/{session.Id}", session);
});

app.MapGet("/api/sessions", () => sessions);

app.MapGet("/api/sessions/{exercise}", (string exercise) =>
{
    var filtered = sessions
        .Where(s => s.Exercise.Equals(exercise, StringComparison.OrdinalIgnoreCase))
        .ToList();
    return Results.Ok(filtered);
});

app.MapGet("/api/progress", (string? period) =>
{
    var now = DateTime.UtcNow;
    var cutoff = period?.ToLower() switch
    {
        "week" => now.AddDays(-7),
        "month" => now.AddMonths(-1),
        "6months" => now.AddMonths(-6),
        "year" => now.AddYears(-1),
        _ => DateTime.MinValue
    };

    var filtered = sessions
        .Where(s => s.Date >= cutoff)
        .GroupBy(s => s.Exercise)
        .Select(g => new
        {
            Exercise = g.Key,
            Sessions = g.OrderBy(s => s.Date).Select(s => new
            {
                s.Date,
                s.WeightKg,
                s.Reps,
                s.Sets
            })
        });

    return Results.Ok(filtered);
});

app.Run();
Test all endpoints with curl

Your endpoints are written. Now you will run the project and hit each endpoint to confirm everything works end to end.

  • Stop any running instance of the app by pressing Ctrl+C in the terminal.
  • Start the project by running:
dotnet run

Look at your terminal output for the URL your app is listening on. It will look something like http://localhost:5xxx. Note this URL for the next commands.

  • Open a second terminal in VS Code by clicking Terminal in the top menu bar, then New Terminal.
  • Test the POST endpoint by running this command (replace the URL with your localhost URL from the terminal output):
curl -X POST [[LOCALHOST_URL="your localhost URL, e.g. http://localhost:5000"]]/api/sessions -H "Content-Type: application/json" -d "{\"exercise\":\"bench press\",\"weightKg\":80,\"reps\":8,\"sets\":3}"

You should see a JSON response containing your session with an auto-assigned id and date field.

  • Test the GET all sessions endpoint by running:
curl [[LOCALHOST_URL="your localhost URL, e.g. http://localhost:5000"]]/api/sessions

You should see a JSON array containing the session you just created.

  • Test the filter-by-exercise endpoint by running:
curl [[LOCALHOST_URL="your localhost URL, e.g. http://localhost:5000"]]/api/sessions/bench%20press

You should see only sessions matching "bench press" returned.

  • Test the progress endpoint with a time filter by running:
curl "[[LOCALHOST_URL="your localhost URL, e.g. http://localhost:5000"]]/api/progress?period=all"

You should see sessions grouped by exercise name, with each group containing an ordered list of sessions.

What should I see?

Each GET response returns JSON data. The progress endpoint groups results by exercise name and orders sessions by date within each group. Since you just created one session, you will see one group with one entry.

Getting connection refused?

Make sure dotnet run is still active in your first terminal. The API only responds while the process is running.

Check that the URL in your curl command matches the one shown in your terminal output exactly, including the port number.

help me debug connection refused errors when testing my ASP.NET Core API with curl

Your API is fully functional. Next up, you will build a voice-enabled web page that speaks workout data directly into these endpoints.

Add the Voice Input Page

Your API endpoints are working, but typing curl commands every time you finish a set at the gym is not practical.

In this step, you will build a browser-based front end that lets you speak your workout data out loud. The browser's Web Speech API captures your voice, and the page sends the data to your API with a single fetch() call.

In this step, get ready to:
  • Create a wwwroot folder and enable static file serving.
  • Build a voice input page that prompts for each workout field one at a time.
  • Wire up fetch calls to POST workout data and display sessions from the API.
Create the wwwroot folder and enable static files

ASP.NET Core serves static files (HTML, CSS, JavaScript) from a special folder called wwwroot. Your Program.cs already includes the middleware that makes this work.

  • Switch back to Program.cs in VS Code and confirm these two lines appear near the top, after var app = builder.Build();:
app.UseDefaultFiles();
app.UseStaticFiles();

What do these two lines do?

UseDefaultFiles() is a URL rewriter. When a request hits the root path (/), it looks inside wwwroot for a file named index.html and rewrites the request to serve it.

UseStaticFiles() actually delivers the files from wwwroot to the browser. The order matters. UseDefaultFiles must come first so the rewrite happens before the file is served.

  • Create the wwwroot folder inside your StrengthTracker project directory by running this command in the terminal:
mkdir wwwroot
  • Create a new file called index.html inside the wwwroot folder by running:
touch wwwroot/index.html
  • Confirm the file exists by running:
ls wwwroot

You should see index.html listed in the output.

Don't see index.html?

Make sure you are in the StrengthTracker project folder (the one containing Program.cs). Run ls and check you can see Program.cs in the same directory.

help me find my project folder so I can create wwwroot

Build the HTML page with guided voice input

The page uses the browser's SpeechRecognition API to listen for one field at a time. It prompts you for the exercise name first, then the weight, reps, and sets.

  • Open wwwroot/index.html in VS Code and paste the following HTML structure:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Strength Tracker</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background: #1a1a2e; color: #eee; }
        h1 { margin-bottom: 20px; color: #00d4ff; }
        button { background: #00d4ff; color: #1a1a2e; border: none; padding: 12px 24px; font-size: 16px; border-radius: 8px; cursor: pointer; margin: 8px 4px; }
        button:hover { background: #00b8e6; }
        .status { margin: 16px 0; padding: 12px; background: #16213e; border-radius: 8px; min-height: 48px; }
        .sessions { margin-top: 24px; }
        .session-card { background: #16213e; padding: 12px; border-radius: 8px; margin: 8px 0; }
        .session-card strong { color: #00d4ff; }
        #prompt { font-size: 18px; color: #ffd700; }
    </style>
</head>
<body>
    <h1>Strength Tracker</h1>

    <button id="startBtn">Start Logging (Voice)</button>
    <button id="viewBtn">View Sessions</button>
    <button id="progressBtn">View Progress</button>

    <div class="status">
        <p id="prompt">Press "Start Logging" and speak your workout.</p>
        <p id="result"></p>
    </div>

    <div>
        <label for="periodSelect">Progress period: </label>
        <select id="periodSelect">
            <option value="week">Last Week</option>
            <option value="month">Last Month</option>
            <option value="6months">Last 6 Months</option>
            <option value="year">Last Year</option>
            <option value="all">All Time</option>
        </select>
    </div>

    <div class="sessions" id="output"></div>

    <script>
    </script>
</body>
</html>

What does this HTML set up?

  • Three buttons: Start Logging (Voice) to begin voice capture, View Sessions to display logged workouts, and View Progress to see progress filtered by time.
  • A status area with a #prompt paragraph that shows which field to speak, and a #result paragraph that displays captured values.
  • A #periodSelect dropdown for choosing the time filter (week, month, 6 months, year, all time).
  • A #output div where session cards will render.

Now you will add the voice recognition logic inside the empty <script> tag.

  • Replace the empty <script> tag with the following code. This sets up the speech recognition and guides the user through each field:
    <script>
        const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
        const prompt = document.getElementById('prompt');
        const result = document.getElementById('result');
        const output = document.getElementById('output');

        const fields = ['exercise', 'weightKg', 'reps', 'sets'];
        const prompts = {
            exercise: 'Say the exercise name (e.g. "chest press")',
            weightKg: 'Say the weight in kg (e.g. "80")',
            reps: 'Say the number of reps (e.g. "8")',
            sets: 'Say the number of sets (e.g. "3")'
        };

        let currentField = 0;
        let sessionData = {};

        document.getElementById('startBtn').addEventListener('click', () => {
            if (!SpeechRecognition) {
                prompt.textContent = 'Speech recognition not supported. Use Edge or Chrome.';
                return;
            }
            currentField = 0;
            sessionData = {};
            result.textContent = '';
            listenForField();
        });

        function listenForField() {
            const field = fields[currentField];
            prompt.textContent = prompts[field];

            const recognition = new SpeechRecognition();
            recognition.lang = 'en-US';
            recognition.interimResults = false;
            recognition.maxAlternatives = 1;

            recognition.onresult = (event) => {
                const transcript = event.results[0][0].transcript.trim();
                if (field === 'exercise') {
                    sessionData[field] = transcript;
                } else {
                    const num = parseFloat(transcript.replace(/[^0-9.]/g, ''));
                    sessionData[field] = isNaN(num) ? 0 : num;
                }
                result.textContent += `${field}: ${sessionData[field]} | `;
                currentField++;
                if (currentField < fields.length) {
                    setTimeout(listenForField, 500);
                } else {
                    submitSession();
                }
            };

            recognition.onerror = (event) => {
                prompt.textContent = `Error: ${event.error}. Try again.`;
            };

            recognition.start();
        }
    </script>

What does the voice recognition code do?

  • SpeechRecognition is grabbed with a fallback for the webkit prefix, covering both Edge and Chrome.
  • The fields array defines the four prompts in order: exercise, weightKg, reps, sets.
  • listenForField() creates a new recognition instance for each field, listens for one result, stores it in sessionData, and advances to the next field.
  • For numeric fields, the transcript is stripped of non-numeric characters with a regex before parsing.
  • After all four fields are captured, it calls submitSession() (which you will add next).

Why Edge or Chrome?

The Web Speech API is supported in Chrome (since version 33) and Edge (since version 79). Firefox does not support it. Since you are on Windows, Edge is pre-installed and ready to use.

Wire the voice input to the API and display sessions

The final piece connects the captured voice data to your API endpoints. You will add the submitSession() function and click handlers for viewing sessions and progress.

  • In wwwroot/index.html, find the closing }); of the recognition.start(); line inside listenForField(). Add the following code below the closing brace of the listenForField function, but still inside the <script> tag:
        async function submitSession() {
            prompt.textContent = 'Saving...';
            const response = await fetch('/api/sessions', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(sessionData)
            });
            if (response.ok) {
                const saved = await response.json();
                prompt.textContent = `Logged: ${saved.exercise} - ${saved.weightKg}kg x ${saved.reps} reps x ${saved.sets} sets`;
                result.textContent = '';
            } else {
                prompt.textContent = 'Failed to save. Try again.';
            }
        }

        document.getElementById('viewBtn').addEventListener('click', async () => {
            const response = await fetch('/api/sessions');
            const data = await response.json();
            output.innerHTML = data.map(s =>
                `<div class="session-card"><strong>${s.exercise}</strong> - ${s.weightKg}kg x ${s.reps} reps x ${s.sets} sets<br><small>${new Date(s.date).toLocaleDateString()}</small></div>`
            ).join('') || '<p>No sessions logged yet.</p>';
        });

        document.getElementById('progressBtn').addEventListener('click', async () => {
            const period = document.getElementById('periodSelect').value;
            const response = await fetch(`/api/progress?period=${period}`);
            const data = await response.json();
            output.innerHTML = data.map(g =>
                `<div class="session-card"><strong>${g.exercise}</strong><br>` +
                g.sessions.map(s => `${new Date(s.date).toLocaleDateString()}: ${s.weightKg}kg x ${s.reps} reps x ${s.sets} sets`).join('<br>') +
                `</div>`
            ).join('') || '<p>No data for this period.</p>';
        });

What does this code do?

  • submitSession() sends the captured sessionData object to POST /api/sessions as JSON. On success, it displays a confirmation with the exercise, weight, reps, and sets.
  • The viewBtn handler fetches all sessions from GET /api/sessions and renders each one as a styled card showing the exercise name, stats, and date.
  • The progressBtn handler reads the selected period from the dropdown, fetches GET /api/progress?period= with that value, and renders grouped results by exercise.
  • Save wwwroot/index.html.
  • Run your project by executing this command in the terminal:
dotnet run
  • Open the localhost URL shown in your terminal output (e.g. http://localhost:5000) in Microsoft Edge.
  • You should see the dark-themed Strength Tracker page with three buttons and a gold prompt message.
  • Click Start Logging (Voice) and allow microphone access when the browser prompts you.
  • Speak each value when prompted (exercise name, weight, reps, sets).
  • After all four values are captured, you should see a confirmation message like Logged: chest press - 80kg x 8 reps x 3 sets.
  • Click View Sessions to confirm your logged session appears as a card below.

Page not loading at the root URL?

If you see the old "Hello World" text or a blank page, make sure app.UseDefaultFiles() and app.UseStaticFiles() appear before any app.Map calls in Program.cs. Also confirm your index.html file is inside the wwwroot folder, not in the project root.

my Strength Tracker page is not loading at the root URL

Speech recognition not working?

Make sure you are using Microsoft Edge or Google Chrome. Firefox does not support the Web Speech API. Also check that you allowed microphone access when the browser prompted you.

help me troubleshoot speech recognition in my browser

✔️ Awesome, I've got everything!

Great. Double check you have saved wwwroot/index.html and your voice logging is working.

ⓧ I'd like to double check the full code

Here is the complete wwwroot/index.html file:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Strength Tracker</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: system-ui, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background: #1a1a2e; color: #eee; }
        h1 { margin-bottom: 20px; color: #00d4ff; }
        button { background: #00d4ff; color: #1a1a2e; border: none; padding: 12px 24px; font-size: 16px; border-radius: 8px; cursor: pointer; margin: 8px 4px; }
        button:hover { background: #00b8e6; }
        .status { margin: 16px 0; padding: 12px; background: #16213e; border-radius: 8px; min-height: 48px; }
        .sessions { margin-top: 24px; }
        .session-card { background: #16213e; padding: 12px; border-radius: 8px; margin: 8px 0; }
        .session-card strong { color: #00d4ff; }
        #prompt { font-size: 18px; color: #ffd700; }
    </style>
</head>
<body>
    <h1>Strength Tracker</h1>

    <button id="startBtn">Start Logging (Voice)</button>
    <button id="viewBtn">View Sessions</button>
    <button id="progressBtn">View Progress</button>

    <div class="status">
        <p id="prompt">Press "Start Logging" and speak your workout.</p>
        <p id="result"></p>
    </div>

    <div>
        <label for="periodSelect">Progress period: </label>
        <select id="periodSelect">
            <option value="week">Last Week</option>
            <option value="month">Last Month</option>
            <option value="6months">Last 6 Months</option>
            <option value="year">Last Year</option>
            <option value="all">All Time</option>
        </select>
    </div>

    <div class="sessions" id="output"></div>

    <script>
        const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
        const prompt = document.getElementById('prompt');
        const result = document.getElementById('result');
        const output = document.getElementById('output');

        const fields = ['exercise', 'weightKg', 'reps', 'sets'];
        const prompts = {
            exercise: 'Say the exercise name (e.g. "chest press")',
            weightKg: 'Say the weight in kg (e.g. "80")',
            reps: 'Say the number of reps (e.g. "8")',
            sets: 'Say the number of sets (e.g. "3")'
        };

        let currentField = 0;
        let sessionData = {};

        document.getElementById('startBtn').addEventListener('click', () => {
            if (!SpeechRecognition) {
                prompt.textContent = 'Speech recognition not supported. Use Edge or Chrome.';
                return;
            }
            currentField = 0;
            sessionData = {};
            result.textContent = '';
            listenForField();
        });

        function listenForField() {
            const field = fields[currentField];
            prompt.textContent = prompts[field];

            const recognition = new SpeechRecognition();
            recognition.lang = 'en-US';
            recognition.interimResults = false;
            recognition.maxAlternatives = 1;

            recognition.onresult = (event) => {
                const transcript = event.results[0][0].transcript.trim();
                if (field === 'exercise') {
                    sessionData[field] = transcript;
                } else {
                    const num = parseFloat(transcript.replace(/[^0-9.]/g, ''));
                    sessionData[field] = isNaN(num) ? 0 : num;
                }
                result.textContent += `${field}: ${sessionData[field]} | `;
                currentField++;
                if (currentField < fields.length) {
                    setTimeout(listenForField, 500);
                } else {
                    submitSession();
                }
            };

            recognition.onerror = (event) => {
                prompt.textContent = `Error: ${event.error}. Try again.`;
            };

            recognition.start();
        }

        async function submitSession() {
            prompt.textContent = 'Saving...';
            const response = await fetch('/api/sessions', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(sessionData)
            });
            if (response.ok) {
                const saved = await response.json();
                prompt.textContent = `Logged: ${saved.exercise} - ${saved.weightKg}kg x ${saved.reps} reps x ${saved.sets} sets`;
                result.textContent = '';
            } else {
                prompt.textContent = 'Failed to save. Try again.';
            }
        }

        document.getElementById('viewBtn').addEventListener('click', async () => {
            const response = await fetch('/api/sessions');
            const data = await response.json();
            output.innerHTML = data.map(s =>
                `<div class="session-card"><strong>${s.exercise}</strong> - ${s.weightKg}kg x ${s.reps} reps x ${s.sets} sets<br><small>${new Date(s.date).toLocaleDateString()}</small></div>`
            ).join('') || '<p>No sessions logged yet.</p>';
        });

        document.getElementById('progressBtn').addEventListener('click', async () => {
            const period = document.getElementById('periodSelect').value;
            const response = await fetch(`/api/progress?period=${period}`);
            const data = await response.json();
            output.innerHTML = data.map(g =>
                `<div class="session-card"><strong>${g.exercise}</strong><br>` +
                g.sessions.map(s => `${new Date(s.date).toLocaleDateString()}: ${s.weightKg}kg x ${s.reps} reps x ${s.sets} sets`).join('<br>') +
                `</div>`
            ).join('') || '<p>No data for this period.</p>';
        });
    </script>
</body>
</html>

Your Program.cs should remain unchanged from the previous step.

Your API now has a proper frontend. Next up, you will deploy the entire application to Azure App Service so it is accessible from anywhere.

Deploy to Azure App Service

Your Strength Tracker API works perfectly on your machine. The next step is getting it live on the internet so you can use it from any device.

Azure App Service hosts web applications without you managing any servers. With a single command, it creates all the cloud infrastructure and pushes your code live.

In this step, get ready to:
  • Deploy the Strength Tracker to Azure App Service with one command.
  • Test voice input on the live URL.
  • Verify the progress endpoint works in production.
Deploy to Azure with a single command

The az webapp up command is a one-step deployment tool. It creates an Azure Resource Group, an App Service Plan, and the App Service itself, then deploys your code to it.

  • In VS Code, open a new terminal by pressing Ctrl+` (backtick key, usually above Tab).
  • Make sure you are in the StrengthTracker project folder. If the terminal opened elsewhere, navigate to it by running:
cd ~/Desktop/StrengthTracker
  • Deploy your app to Azure by running this command:
az webapp up --sku F1 --name [[APP_NAME="your-unique-app-name"]] --os-type windows

What does this command do?

  • --sku F1 selects the Free tier App Service Plan. This costs $0 and gives you 60 CPU minutes per day.
  • --name sets the globally unique name for your app. This becomes part of your live URL.
  • --os-type windows tells Azure to use a Windows server to host your .NET app.
  • Azure auto-creates a Resource Group and App Service Plan if they don't already exist.

Pick a unique app name

Your app name must be unique across all of Azure. Try something like strengthtracker-yourname or strengthtracker-2026. If the name is taken, Azure will tell you and you can try a different one.

  • Wait for the deployment to complete. This takes 1-3 minutes.
  • When it finishes, look for the URL in the output. It follows the format https://your-app-name.azurewebsites.net.

Seeing a deployment error?

  • If you see a message about the name being taken, choose a different value for --name and run the command again.
  • If you see an authentication error, run az login again to refresh your session, then retry the deploy command.
  • Make sure your terminal is inside the StrengthTracker folder (the one containing Program.cs).

help me troubleshoot my az webapp up deployment error

Test voice input on the live URL

Your app is now live on the internet. The Web Speech API requires HTTPS to work, and Azure App Service provides HTTPS by default. This means your voice input will work on the live URL.

  • Open Microsoft Edge or Google Chrome (the Web Speech API does not work in Firefox).
  • Navigate to your live app URL: https://your-app-name.azurewebsites.net.
  • Click Start Logging (Voice).
  • Your browser will ask for microphone permission. Click Allow.
  • Speak an exercise name when prompted (for example, say "bench press").
  • Continue speaking the weight, reps, and sets when each prompt appears.
  • After all four fields are captured, you should see a confirmation message showing your logged session.
  • Click View Sessions to confirm your session was saved to the live API.

You should see a session card with the exercise name, weight, reps, and sets you just spoke.

Voice input not working on the live site?

  • Make sure you are using Edge or Chrome. Firefox does not support the Web Speech API.
  • Check that you allowed microphone permission when the browser prompted you.
  • If you see "Speech recognition not supported", try refreshing the page. Some browsers need a fresh page load after granting permission.

help me fix voice input on my deployed Azure app

Verify the progress endpoint in production

The progress endpoint groups your sessions by exercise and filters them by time period. Testing this confirms your API handles query parameters correctly in production.

  • On your live app page, select All Time from the Progress period dropdown.
  • Click View Progress.

You should see your session grouped under its exercise name, with the date, weight, reps, and sets displayed.

  • To test the API endpoint directly, open a new browser tab and navigate to:
https://[[APP_NAME="your-unique-app-name"]].azurewebsites.net/api/progress?period=all
  • You should see a JSON response with your sessions grouped by exercise.

Why does data reset on restart?

Your app uses in-memory storage. This means sessions are lost whenever Azure restarts the app (which happens during scaling or inactivity on the free tier).

For a production app, you would add a database like Azure SQL or SQLite. For this project, in-memory storage keeps things simple so you can focus on learning API design and deployment.

No data showing in progress?

  • Make sure you logged at least one session with voice input before clicking View Progress.
  • If you used a different time filter (like "Last Week"), switch to All Time to see all logged sessions.
  • If your app restarted between logging and viewing, the in-memory data was cleared. Log a new session and try again.

help me troubleshoot my progress endpoint on Azure

Your Strength Tracker is live on the internet. You built a real API from scratch, added voice input, and deployed it to the cloud. Next up is an optional Secret Mission to push your C# skills further.

Secret mission

Add a Personal Records Endpoint

Your API tracks every session, but what about your all-time bests? Add a personal records endpoint that finds the heaviest lift you have ever logged for each exercise. Then wire up a button on your voice page so you can check your PRs at a glance.

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. This project uses the Azure App Service Free F1 tier, so there are no ongoing costs while your app stays deployed.

Resources you used:

  • Azure Resource Group (auto-created by az webapp up).
  • Azure App Service Plan (Free F1 tier).
  • Azure App Service (your deployed web app).
  • Local StrengthTracker project folder on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep your live API available.

  • The Free F1 tier costs $0, so your app can stay deployed indefinitely.
  • Session data is stored in memory and resets when the app restarts.
  • Your local project files remain in the StrengthTracker folder on your Desktop.
  • The .NET 10 SDK, Azure CLI, and C# Dev Kit extension remain installed on your machine for future projects.

Pause - I'll come back to this later

Stop the running app to free up your shared compute minutes, but keep all resources and files intact so you can resume later.

  • Stop the Azure App Service by running this command from your StrengthTracker project folder:
az webapp stop
  • When you are ready to resume, start the app again by running:
az webapp start
  • Your local project files stay in the StrengthTracker folder on your Desktop.

Delete - I don't want to use this again

Remove all Azure resources and local project files to start completely fresh.

  • Delete all Azure resources (Resource Group, App Service Plan, and App Service) by running this command from your StrengthTracker project folder:
az group delete --yes

Why no --name flag?

The az webapp up command you ran earlier cached the resource group name in your project's .azure/config file. Running az group delete from the same folder automatically targets that cached group.

  • This command may take a minute to complete. It removes the Resource Group and everything inside it.
  • Delete your local project folder. On macOS or Linux, run:
cd ~/Desktop
rm -rf StrengthTracker
  • On Windows, run:
cd ~/Desktop
Remove-Item -Recurse -Force StrengthTracker
  • The .NET 10 SDK, Azure CLI, and C# Dev Kit extension will remain installed. These are general development tools you will likely use again.

Nice Work!

Nice Work!

Nice work! You just built and deployed a voice-powered strength tracker API from scratch using C# and ASP.NET Core.

You've learned how to:

  • Build a REST API from scratch using C# and ASP.NET Core Minimal APIs, with multiple endpoints that handle filtering and query parameters.
  • Serve a static HTML page with voice input powered by the Web Speech API, turning spoken workout data into structured API requests.
  • Deploy the entire application to Azure App Service using the Azure CLI, taking your project from localhost to a live public URL.
  • Secret Mission: Added a personal records endpoint that uses LINQ grouping to surface your all-time heaviest lift per exercise.

Ready to quiz yourself?