Build a Helpdesk Ticket System

Build a full-stack helpdesk app with ASP.NET Core Web API and React.

Introduction

30 Second Summary

Every company has a system for tracking internal issues. Whether it's IT support, customer complaints, or bug reports, someone needs to file a ticket and someone needs to resolve it.

In this project, you will build a helpdesk ticket system with an ASP.NET Core backend API and a React frontend that lets users create, view, update, and delete support tickets.

What You'll Build

A full-stack helpdesk application with an ASP.NET Core Web API backend using Entity Framework Core for data management and a React single-page application built with Vite for the frontend.

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

  • An ASP.NET Core Web API with full CRUD endpoints for creating, reading, updating, and deleting support tickets.
  • A React SPA that fetches and displays tickets from your API with components for listing, creating, and managing ticket status.
  • A CORS configuration connecting your frontend and backend during local development.
  • Secret Mission: Add a priority system with color-coded filtering so users can sort tickets by urgency.

Are there any prerequisites?

This project assumes basic familiarity with C# and JavaScript/React syntax. You do not need prior experience connecting a frontend to a backend.

Set Up Your Development Environment

Building a full-stack helpdesk application requires both .NET and Node.js tooling working together on your machine.

Visual Studio 2026 Community bundles both the .NET 10 SDK and Node.js when you select the right workloads during installation. In this step, you'll verify your environment has the correct versions installed and confirm the project template is available.

In this step, get ready to:
  • Confirm Visual Studio 2026 Community is installed with the required workloads.
  • Verify .NET 10 SDK and Node.js versions in the terminal.
  • Locate the ASP.NET Core Web API project template.
Install Visual Studio 2026 Community

Visual Studio 2026 Community is a free, full-featured IDE that includes the .NET 10 SDK and Node.js tooling when you install the correct workloads.

  • If you haven't already, download Visual Studio 2026 Community from visualstudio.microsoft.com/downloads.
  • During installation (or via the Visual Studio Installer if already installed), make sure these two workloads are selected:
  • Click Install or Modify to apply the workloads if any are missing.

Why these workloads?

The "ASP.NET and web development" workload installs the .NET 10 SDK, project templates for Web APIs, and the runtime you need to build and run your backend.

The "Node.js development" workload installs Node.js, which you'll need later to scaffold and run your React frontend with Vite.

Verify .NET and Node.js

With the workloads installed, the .NET 10 SDK and Node.js should both be available from any terminal.

  • Press the Windows key to open your search bar.
  • Type Windows Terminal and press Enter to open it.
  • Verify the .NET 10 SDK is installed by running this command:
dotnet --version

You should see a version number starting with 10, such as 10.0.300.

  • Verify Node.js is installed by running this command:
node --version

You should see v20.19.0 or higher.

Command not found?

If dotnet or node is not recognized, the workloads may not have installed correctly.

  • Open the Visual Studio Installer (search for "Visual Studio Installer" in the Start menu).
  • Click Modify on your Visual Studio 2026 Community installation.
  • Confirm both ASP.NET and web development and Node.js development are checked, then click Modify.
  • Close and reopen Windows Terminal, then try the commands again.

Still stuck? Help me troubleshoot why dotnet or node commands are not found after installing Visual Studio 2026 workloads.

Confirm the setup

The final check is confirming that Visual Studio can see the ASP.NET Core Web API template you'll use in the next step.

  • Press the Windows key to open your search bar.
  • Type Visual Studio 2026 and press Enter to open it.
  • On the start window, click Create a new project.
  • In the search bar at the top, type ASP.NET Core Web API.
  • Confirm you see the ASP.NET Core Web API template in the results list (with C# and a cloud icon).

What is the ASP.NET Core Web API template?

This template scaffolds a ready-to-run REST API project with controllers, routing, and Swagger documentation pre-configured.

You'll use it in the next step to create the backend for your helpdesk system.

Don't see the template?

If the ASP.NET Core Web API template does not appear, the "ASP.NET and web development" workload may be missing.

  • Close Visual Studio.
  • Open the Visual Studio Installer and click Modify.
  • Check the ASP.NET and web development workload and click Modify to install it.
  • Reopen Visual Studio and search for the template again.

Need more help? Help me find the ASP.NET Core Web API template in Visual Studio 2026.

Your development environment is ready to go. Next up, you'll use that ASP.NET Core Web API template to create the backend for your helpdesk system.

Create the ASP.NET Core Web API Backend

Every full-stack application needs a backend that stores data and serves it to the frontend. Your helpdesk system is no different.

In this step, you will scaffold an ASP.NET Core Web API project, define a data model for tickets, and configure Entity Framework Core with an in-memory database. You will also set up CORS so the React frontend can talk to this backend later.

In this step, get ready to:
  • Create an ASP.NET Core Web API project in Visual Studio.
  • Define a Ticket model and database context.
  • Configure the in-memory database and CORS policy.
Scaffold the API project

The ASP.NET Core Web API template gives you a pre-configured project with controllers, Swagger documentation, and routing already wired up. This saves you from writing boilerplate configuration by hand.

  • In Visual Studio 2026, click File in the top menu bar.
  • Click New, then Project.
  • In the Create a new project dialog, type Web API in the search box.
  • Select the ASP.NET Core Web API template and click Next.
  • In the Configure your new project window, enter HelpdeskApi as the Project name and click Next.
  • In the Additional information dialog, confirm the Framework is set to .NET 10.0.
  • Confirm the checkbox for Use controllers (uncheck to use Minimal APIs) is checked.
  • Confirm the checkbox for Enable OpenAPI support is checked.
  • Click Create.

Visual Studio generates the project and opens it in Solution Explorer.

What did this create?

The template created a Program.cs file with basic service registration, a Controllers folder, and a .csproj project file. It also included Swashbuckle for Swagger API documentation out of the box.

Now you need to install the Entity Framework Core in-memory database package so the API can store ticket data without needing a full database server.

  • From the Tools menu, select NuGet Package Manager, then Manage NuGet Packages for Solution.
  • Select the Browse tab.
  • Type Microsoft.EntityFrameworkCore.InMemory in the search box.
  • Select Microsoft.EntityFrameworkCore.InMemory from the results list.
  • Check the HelpdeskApi project checkbox in the right pane, then click Install.
  • If prompted to accept license terms, click I Accept.

After installation, you should see Microsoft.EntityFrameworkCore.InMemory listed under Dependencies > Packages in Solution Explorer.

Package not appearing in search results?

Make sure nuget.org is selected as the Package source dropdown at the top-right of the NuGet Package Manager window. If it is missing, go to Tools > NuGet Package Manager > Package Manager Settings > Package Sources and add https://api.nuget.org/v3/index.json as a source.

Help me troubleshoot NuGet package installation in Visual Studio.

Define the Ticket model and DbContext

Your API needs a data shape that describes what a helpdesk ticket looks like. In Entity Framework Core, you define this as a model class and then expose it through a DbContext.

  • In Solution Explorer, right-click on the HelpdeskApi project.
  • Select Add, then New Folder.
  • Name the folder Models.
  • Right-click the Models folder and select Add, then Class.
  • Name the class Ticket and click Add.
  • Replace the entire contents of the file with the following code:
namespace HelpdeskApi.Models;

public class Ticket
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public string Status { get; set; } = "Open";
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

What does this code do?

  • Each property represents a column in the tickets table: Id is the primary key, Title and Description store the ticket content.
  • Status defaults to "Open" so every new ticket starts in the open state.
  • CreatedAt records when the ticket was submitted, defaulting to the current UTC time.
  • Save Ticket.cs.

Now create the database context that Entity Framework Core uses to interact with your tickets.

  • Right-click the HelpdeskApi project in Solution Explorer.
  • Select Add, then New Folder.
  • Name the folder Data.
  • Right-click the Data folder and select Add, then Class.
  • Name the class HelpdeskDbContext and click Add.
  • Replace the entire contents of the file with the following code:
using Microsoft.EntityFrameworkCore;
using HelpdeskApi.Models;

namespace HelpdeskApi.Data;

public class HelpdeskDbContext : DbContext
{
    public HelpdeskDbContext(DbContextOptions<HelpdeskDbContext> options)
        : base(options) { }

    public DbSet<Ticket> Tickets => Set<Ticket>();
}

What does this code do?

A DbContext is how Entity Framework Core manages your database connection and maps classes to tables.

  • The constructor accepts DbContextOptions so the database provider (in-memory, in this case) can be configured externally.
  • DbSet<Ticket> Tickets exposes the tickets table so controllers can query and modify ticket data.
  • Save HelpdeskDbContext.cs.

Seeing a red underline on DbContext or DbSet?

This means the Microsoft.EntityFrameworkCore namespace is not resolving. Confirm that the Microsoft.EntityFrameworkCore.InMemory NuGet package installed successfully by checking Dependencies > Packages in Solution Explorer.

Help me fix unresolved Entity Framework references in my project.

Register the DbContext with the in-memory provider

The model and context classes exist, but ASP.NET Core does not know about them yet. You need to register the DbContext in the app's service container and also configure CORS so the React dev server at http://localhost:5173 can call this API.

  • Open Program.cs from Solution Explorer (it is at the project root).
  • Replace the entire contents of the file with the following code:
using Microsoft.EntityFrameworkCore;
using HelpdeskApi.Data;
using HelpdeskApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<HelpdeskDbContext>(opt =>
    opt.UseInMemoryDatabase("HelpdeskDb"));

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowReactDev", policy =>
    {
        policy.WithOrigins("http://localhost:5173")
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseCors("AllowReactDev");
app.MapControllers();

app.Run();

What does this code do?

  • AddDbContext<HelpdeskDbContext> registers the database context and tells it to use an in-memory database named "HelpdeskDb". Data lives in memory and resets on every restart.
  • AddCors creates a policy called "AllowReactDev" that permits requests from http://localhost:5173 (the Vite dev server's default port).
  • UseCors("AllowReactDev") activates the policy in the middleware pipeline so the browser allows cross-origin requests.
  • UseSwagger and UseSwaggerUI expose an interactive API documentation page in development mode.

Why an in-memory database?

The EF Core in-memory provider removes the need for database file management and migrations. You can focus on API and React integration without worrying about database administration. The tradeoff is that data resets every time you stop the application.

  • Save Program.cs.
  • Build the project by pressing Ctrl+Shift+B.

You should see "Build succeeded" in the Output panel at the bottom of Visual Studio with no errors.

Build failed with errors?

Check that your Program.cs has the three using statements at the top: Microsoft.EntityFrameworkCore, HelpdeskApi.Data, and HelpdeskApi.Models. Also confirm you named your folders exactly Models and Data (with capital first letters) since C# namespaces are case-sensitive.

Help me fix build errors in my ASP.NET Core project.

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files: Ticket.cs, HelpdeskDbContext.cs, and Program.cs.

ⓧ I'd like to double check the full code

Here is each file as it should look at this point.

namespace HelpdeskApi.Models;

public class Ticket
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public string Status { get; set; } = "Open";
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
using Microsoft.EntityFrameworkCore;
using HelpdeskApi.Models;

namespace HelpdeskApi.Data;

public class HelpdeskDbContext : DbContext
{
    public HelpdeskDbContext(DbContextOptions<HelpdeskDbContext> options)
        : base(options) { }

    public DbSet<Ticket> Tickets => Set<Ticket>();
}
using Microsoft.EntityFrameworkCore;
using HelpdeskApi.Data;
using HelpdeskApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<HelpdeskDbContext>(opt =>
    opt.UseInMemoryDatabase("HelpdeskDb"));

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowReactDev", policy =>
    {
        policy.WithOrigins("http://localhost:5173")
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseCors("AllowReactDev");
app.MapControllers();

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

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

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.0" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
  </ItemGroup>

</Project>

Your backend foundation is in place. Next up, you will build the API controller that exposes ticket endpoints for creating, reading, updating, and deleting tickets.

Build the Tickets API Controller

Your data model and database context are ready. Now you need a way for the outside world to interact with your tickets.

In ASP.NET Core, a controller is the class that receives HTTP requests and returns responses. You will build a TicketsController that exposes CRUD endpoints (Create, Read, Update, Delete) for your helpdesk tickets, then seed the database with sample data so you have something to work with immediately.

In this step, get ready to:
  • Create API endpoints for managing tickets.
  • Seed the database with sample tickets on startup.
  • Test the API using Swagger UI.
Create the TicketsController with read endpoints

A REST API controller maps HTTP verbs (GET, POST, PUT, DELETE) to C# methods. You will start with the GET endpoints so the API can return ticket data.

  • In Solution Explorer, right-click the HelpdeskApi project.
  • Select Add, then New Folder.
  • Name the folder Controllers and press Enter.
  • Right-click the new Controllers folder and select Add, then Class.
  • Name the file TicketsController.cs and click Add.
  • Replace everything in the new file with the following code:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using HelpdeskApi.Data;
using HelpdeskApi.Models;

namespace HelpdeskApi.Controllers;

[ApiController]
[Route("api/[controller]")]
public class TicketsController : ControllerBase
{
    private readonly HelpdeskDbContext _context;

    // Inject the database context via constructor
    public TicketsController(HelpdeskDbContext context)
    {
        _context = context;
    }

    // GET: api/tickets - returns all tickets, newest first
    [HttpGet]
    public async Task<ActionResult<IEnumerable<Ticket>>> GetTickets()
    {
        return await _context.Tickets.OrderByDescending(t => t.CreatedAt).ToListAsync();
    }

    // GET: api/tickets/5 - returns a single ticket by ID
    [HttpGet("{id}")]
    public async Task<ActionResult<Ticket>> GetTicket(int id)
    {
        var ticket = await _context.Tickets.FindAsync(id);
        if (ticket == null) return NotFound();
        return ticket;
    }
}

What does this code do?

The [ApiController] attribute tells ASP.NET Core this class handles HTTP requests. The [Route("api/[controller]")] attribute maps all requests starting with /api/tickets to this controller.

The constructor accepts a HelpdeskDbContext parameter. ASP.NET Core's dependency injection system automatically provides this when the controller is created.

The two GET methods handle different requests. GetTickets() returns all tickets sorted by newest first. GetTicket(int id) finds a single ticket or returns a 404 if it does not exist.

  • Save TicketsController.cs by pressing Ctrl+S.

You will not see a visible change yet because the write endpoints still need to be added.

Add create, update, and delete endpoints

With read operations working, you now need endpoints that let users create new tickets, change a ticket's status, and remove resolved tickets.

  • In TicketsController.cs, place your cursor after the closing brace of the GetTicket method (but still inside the class). Add the following code:
    // POST: api/tickets - creates a new ticket
    [HttpPost]
    public async Task<ActionResult<Ticket>> CreateTicket(Ticket ticket)
    {
        ticket.CreatedAt = DateTime.UtcNow;
        ticket.Status = "Open";
        _context.Tickets.Add(ticket);
        await _context.SaveChangesAsync();
        return CreatedAtAction(nameof(GetTicket), new { id = ticket.Id }, ticket);
    }

    // PUT: api/tickets/5 - updates a ticket's status
    [HttpPut("{id}")]
    public async Task<IActionResult> UpdateTicket(int id, Ticket ticket)
    {
        if (id != ticket.Id) return BadRequest();

        var existingTicket = await _context.Tickets.FindAsync(id);
        if (existingTicket == null) return NotFound();

        existingTicket.Status = ticket.Status;
        await _context.SaveChangesAsync();
        return NoContent();
    }

    // DELETE: api/tickets/5 - removes a ticket
    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteTicket(int id)
    {
        var ticket = await _context.Tickets.FindAsync(id);
        if (ticket == null) return NotFound();

        _context.Tickets.Remove(ticket);
        await _context.SaveChangesAsync();
        return NoContent();
    }

What does this code do?

The CreateTicket method sets default values (status and timestamp), saves the new ticket to the database, and returns a 201 Created response with the ticket's URL.

The UpdateTicket method validates that the URL id matches the request body, then updates only the status field. This is intentional because the React frontend will only change a ticket's status.

The DeleteTicket method removes a ticket permanently. It returns 204 No Content on success or 404 if the ticket does not exist.

  • Save TicketsController.cs.
  • Confirm there are no red squiggles (error underlines) in the editor. If you see any, check that your closing braces match the class and each method.

Seeing red squiggles or build errors?

Make sure the three write methods are inside the TicketsController class (between the opening { after ControllerBase and the final }). Check that you have exactly one closing brace for the class at the very end of the file.

If you see errors about missing namespaces, verify the four using statements at the top of the file match exactly.

Help me debug my TicketsController build errors.

✔️ 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

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using HelpdeskApi.Data;
using HelpdeskApi.Models;

namespace HelpdeskApi.Controllers;

[ApiController]
[Route("api/[controller]")]
public class TicketsController : ControllerBase
{
    private readonly HelpdeskDbContext _context;

    public TicketsController(HelpdeskDbContext context)
    {
        _context = context;
    }

    [HttpGet]
    public async Task<ActionResult<IEnumerable<Ticket>>> GetTickets()
    {
        return await _context.Tickets.OrderByDescending(t => t.CreatedAt).ToListAsync();
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Ticket>> GetTicket(int id)
    {
        var ticket = await _context.Tickets.FindAsync(id);
        if (ticket == null) return NotFound();
        return ticket;
    }

    [HttpPost]
    public async Task<ActionResult<Ticket>> CreateTicket(Ticket ticket)
    {
        ticket.CreatedAt = DateTime.UtcNow;
        ticket.Status = "Open";
        _context.Tickets.Add(ticket);
        await _context.SaveChangesAsync();
        return CreatedAtAction(nameof(GetTicket), new { id = ticket.Id }, ticket);
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> UpdateTicket(int id, Ticket ticket)
    {
        if (id != ticket.Id) return BadRequest();

        var existingTicket = await _context.Tickets.FindAsync(id);
        if (existingTicket == null) return NotFound();

        existingTicket.Status = ticket.Status;
        await _context.SaveChangesAsync();
        return NoContent();
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteTicket(int id)
    {
        var ticket = await _context.Tickets.FindAsync(id);
        if (ticket == null) return NotFound();

        _context.Tickets.Remove(ticket);
        await _context.SaveChangesAsync();
        return NoContent();
    }
}
Seed sample data on startup

An empty database is not very useful for testing. You will add a seeding block to Program.cs that populates three sample tickets every time the application starts.

  • Switch back to Program.cs in the editor.
  • Find the line that says:
app.Run();
  • Add the following code directly above app.Run();:
// Seed the database with sample tickets for testing
using (var scope = app.Services.CreateScope())
{
    var context = scope.ServiceProvider.GetRequiredService<HelpdeskDbContext>();
    context.Tickets.AddRange(
        new Ticket { Id = 1, Title = "Cannot login to system", Description = "Getting 401 error when trying to login with correct credentials.", Status = "Open", CreatedAt = DateTime.UtcNow.AddHours(-2) },
        new Ticket { Id = 2, Title = "Email notifications not working", Description = "Users are not receiving email notifications for new assignments.", Status = "In Progress", CreatedAt = DateTime.UtcNow.AddHours(-5) },
        new Ticket { Id = 3, Title = "Dashboard loading slowly", Description = "The main dashboard takes over 10 seconds to load.", Status = "Open", CreatedAt = DateTime.UtcNow.AddDays(-1) }
    );
    context.SaveChanges();
}

What does this code do?

The using (var scope = ...) block creates a temporary service scope so you can request the database context outside of an HTTP request.

Inside the scope, AddRange inserts three tickets with different statuses and timestamps. Because you are using an in-memory database, this data resets every time you restart the application.

  • Save Program.cs.

Build error mentioning HelpdeskDbContext or Ticket?

Check that using HelpdeskApi.Data; and using HelpdeskApi.Models; are both at the top of Program.cs. These were added in the previous step but may have been accidentally removed.

Help me fix my Program.cs build error.

✔️ Awesome, I've got everything!

Great. Double check you have saved Program.cs before running the app.

ⓧ I'd like to double check the full code

using Microsoft.EntityFrameworkCore;
using HelpdeskApi.Data;
using HelpdeskApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<HelpdeskDbContext>(opt =>
    opt.UseInMemoryDatabase("HelpdeskDb"));

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowReactDev", policy =>
    {
        policy.WithOrigins("http://localhost:5173")
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseCors("AllowReactDev");
app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var context = scope.ServiceProvider.GetRequiredService<HelpdeskDbContext>();
    context.Tickets.AddRange(
        new Ticket { Id = 1, Title = "Cannot login to system", Description = "Getting 401 error when trying to login with correct credentials.", Status = "Open", CreatedAt = DateTime.UtcNow.AddHours(-2) },
        new Ticket { Id = 2, Title = "Email notifications not working", Description = "Users are not receiving email notifications for new assignments.", Status = "In Progress", CreatedAt = DateTime.UtcNow.AddHours(-5) },
        new Ticket { Id = 3, Title = "Dashboard loading slowly", Description = "The main dashboard takes over 10 seconds to load.", Status = "Open", CreatedAt = DateTime.UtcNow.AddDays(-1) }
    );
    context.SaveChanges();
}

app.Run();
Test the API with Swagger UI

Your backend is complete. Time to run it and confirm every endpoint works using the built-in Swagger interface.

  • Press F5 in Visual Studio to build and run the project.
  • A browser window opens automatically with the Swagger UI page.
  • In Swagger UI, click GET /api/Tickets to expand it.
  • Click Try it out, then click Execute.

You should see a JSON response containing three tickets. The first ticket has the title "Cannot login to system" and a status of "Open".

What is Swagger UI?

Swagger UI is an interactive documentation page that ASP.NET Core generates automatically for your API. It lets you send requests to every endpoint without writing any frontend code or using a separate tool.

Browser did not open or page is blank?

Check the Output window in Visual Studio for the URL the app is running on. Copy that URL and append /swagger to it in your browser manually.

If you see a build error, check the Error List panel at the bottom of Visual Studio. Double-click any error to jump to the offending line.

Help me troubleshoot my API startup issue.

Your backend API is live and serving ticket data. Next up, you will scaffold the React frontend that consumes these endpoints.

Scaffold the React Frontend with Vite

Your API backend is live and serving ticket data. Now you need a frontend that can display those tickets and let users interact with them.

In this step, you will scaffold a React single-page application using Vite, a fast build tool for modern web projects. You will also create a utility module that handles all communication between the frontend and your API.

In this step, get ready to:
  • Create a React project with Vite in a helpdesk-client folder.
  • Install dependencies and remove boilerplate files.
  • Set up an API client module that connects to your backend.
Create the React project

Vite is a modern build tool that scaffolds React projects with fast hot-reload during development. You will use it to generate the frontend application structure.

  • In Visual Studio 2026, open the integrated terminal by clicking View in the top menu bar, then selecting Terminal.
  • Scaffold the React frontend by running this command:
npm create vite@latest helpdesk-client -- --template react

What does this command do?

  • npm create vite@latest downloads and runs the Vite project scaffolder.
  • helpdesk-client is the folder name for your new React project.
  • -- --template react tells Vite to use the React JavaScript template, skipping the framework selection prompt.

What prompts will I see?

Vite may ask "Install with npm and start now?" after scaffolding. Select No for this prompt. You will install dependencies manually in the next substep.

  • Confirm you see a success message showing the helpdesk-client folder was created.

Don't see the helpdesk-client folder?

  • Make sure the terminal is open at the solution root (the folder containing your HelpdeskApi project folder). If not, navigate to it first.
  • Check that Node.js is installed by running node --version. You need version 20.19+ or 22.12+.

Help me troubleshoot why npm create vite is not creating my helpdesk-client folder.

Install dependencies and clean up boilerplate

The Vite template includes default logos, sample CSS, and demo content that you do not need. You will install the project packages, then strip out the boilerplate to start fresh.

  • Move into the new project folder by running:
cd helpdesk-client
  • Install the npm dependencies by running:
npm install
  • Confirm the install completed by checking that a node_modules folder now exists. Run:
ls node_modules
  • Remove the boilerplate files that Vite generated by running:
rm src/App.css src/index.css src/assets/react.svg
  • Create a components folder inside src for your React components by running:
mkdir src/components
  • Open src/main.jsx in the editor. Remove the line that imports index.css so the file looks like this:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);

What does main.jsx do?

This is the entry point for the React application. It mounts your App component into the HTML page's root element. StrictMode enables extra development checks that help catch common bugs early.

  • Save src/main.jsx.

Can't find main.jsx?

Make sure you are looking inside the helpdesk-client/src folder. If the file does not exist, the Vite scaffold may not have completed. Re-run the npm create vite@latest command from the previous substep.

Help me find or recreate main.jsx in my Vite React project.

Set up the API client

Instead of scattering fetch calls throughout your components, you will create a single utility module that handles all API communication. This keeps your code organized and makes the base URL easy to change later.

  • Create a new file called api.js inside helpdesk-client/src and paste the following code:
const API_BASE = 'http://localhost:5000/api';

// Fetch all tickets from the backend
export async function getTickets() {
  const response = await fetch(`${API_BASE}/tickets`);
  return response.json();
}

// Fetch a single ticket by its ID
export async function getTicket(id) {
  const response = await fetch(`${API_BASE}/tickets/${id}`);
  return response.json();
}

// Create a new ticket with a title and description
export async function createTicket(ticket) {
  const response = await fetch(`${API_BASE}/tickets`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(ticket),
  });
  return response.json();
}

// Update an existing ticket's status
export async function updateTicketStatus(id, status) {
  await fetch(`${API_BASE}/tickets/${id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ id, status }),
  });
}

// Delete a ticket by its ID
export async function deleteTicket(id) {
  await fetch(`${API_BASE}/tickets/${id}`, {
    method: 'DELETE',
  });
}

What does this code do?

  • API_BASE stores the backend URL so you only define it once.
  • getTickets() calls the GET endpoint to retrieve all tickets.
  • createTicket() sends a POST request with ticket data in JSON format.
  • updateTicketStatus() sends a PUT request to change a ticket's status.
  • deleteTicket() sends a DELETE request to remove a ticket from the database.

Each function maps directly to one of the API endpoints you built in the TicketsController earlier.

Why a separate API module?

Centralizing your fetch logic in one file means you only update the base URL in one place if it ever changes. It also keeps your React components focused on rendering UI instead of handling HTTP details.

  • Save src/api.js.
  • Confirm the file exists by running:
ls src/api.js
  • You should see src/api.js listed in the output.

Seeing 'No such file or directory'?

Make sure your terminal is inside the helpdesk-client folder. Run pwd to check your current location. If you are in the solution root, run cd helpdesk-client first.

Help me fix the file not found error for api.js in my Vite project.

✔️ Awesome, I've got everything!

Great. Double check you have saved both src/main.jsx and src/api.js before moving on.

ⓧ I'd like to double check the full code

Here is the complete src/api.js for comparison:

const API_BASE = 'http://localhost:5000/api';

export async function getTickets() {
  const response = await fetch(`${API_BASE}/tickets`);
  return response.json();
}

export async function getTicket(id) {
  const response = await fetch(`${API_BASE}/tickets/${id}`);
  return response.json();
}

export async function createTicket(ticket) {
  const response = await fetch(`${API_BASE}/tickets`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(ticket),
  });
  return response.json();
}

export async function updateTicketStatus(id, status) {
  await fetch(`${API_BASE}/tickets/${id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ id, status }),
  });
}

export async function deleteTicket(id) {
  await fetch(`${API_BASE}/tickets/${id}`, {
    method: 'DELETE',
  });
}

And here is the complete src/main.jsx:

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);

Your frontend project is scaffolded and ready to go. Next up, you will build the React components that display tickets and let users create new ones.

Build the Ticket List and Creation Components

Your backend API is running and your api.js utility is ready to communicate with it. Now it's time to build the React components that bring tickets to life in the browser.

A helpdesk app needs two main pieces of UI: a list that shows all open tickets and a form for submitting new ones. You'll build each as a separate React component, then wire them together in the main App layout with styling.

In this step, get ready to:
  • Create the TicketList component to fetch and display tickets from the API.
  • Create the TicketForm component for submitting new support tickets.
  • Wire both components into App with a grid layout and helpdesk styling.
Create the TicketList component

The TicketList component is the primary display piece. It fetches all tickets from the backend every time its parent signals a refresh, then renders each ticket as a card with status information and action buttons.

  • Create a new file called TicketList.jsx inside the helpdesk-client/src/components folder.
  • Add the following code to TicketList.jsx:
import { useState, useEffect } from 'react';
import { getTickets, deleteTicket, updateTicketStatus } from '../api';

function TicketList({ refreshKey, onRefresh }) {
  // Store the array of tickets fetched from the API
  const [tickets, setTickets] = useState([]);

  // Re-fetch tickets whenever refreshKey changes
  useEffect(() => {
    getTickets().then(setTickets);
  }, [refreshKey]);

  // Advance ticket status: Open -> In Progress -> Resolved
  const handleStatusChange = async (ticket) => {
    const nextStatus = ticket.status === 'Open' ? 'In Progress' : 'Resolved';
    await updateTicketStatus(ticket.id, nextStatus);
    onRefresh();
  };

  // Remove a resolved ticket from the database
  const handleDelete = async (id) => {
    await deleteTicket(id);
    onRefresh();
  };

  return (
    <div className="ticket-list">
      <h2>Support Tickets</h2>
      {tickets.length === 0 && <p>No tickets found.</p>}
      {tickets.map((ticket) => (
        <div key={ticket.id} className={`ticket-card status-${ticket.status.replace(' ', '-').toLowerCase()}`}>
          <div className="ticket-header">
            <h3>{ticket.title}</h3>
            <span className="status-badge">{ticket.status}</span>
          </div>
          <p>{ticket.description}</p>
          <div className="ticket-actions">
            {ticket.status !== 'Resolved' && (
              <button onClick={() => handleStatusChange(ticket)}>
                {ticket.status === 'Open' ? 'Start Progress' : 'Resolve'}
              </button>
            )}
            {ticket.status === 'Resolved' && (
              <button className="delete-btn" onClick={() => handleDelete(ticket.id)}>
                Delete
              </button>
            )}
          </div>
          <small>Created: {new Date(ticket.createdAt).toLocaleString()}</small>
        </div>
      ))}
    </div>
  );
}

export default TicketList;

What does this code do?

  • useEffect runs getTickets() whenever refreshKey changes, keeping the displayed list in sync with the backend.
  • handleStatusChange advances a ticket from Open to In Progress, or from In Progress to Resolved, using the PUT endpoint.
  • handleDelete removes a resolved ticket by calling the DELETE endpoint, then triggers a list refresh.
  • The JSX maps over the tickets array and renders a card for each one. A dynamic CSS class based on the ticket status controls the left-border color.
  • Save TicketList.jsx.
  • You won't see a change in the browser yet. The next component and App.jsx will bring everything together.

Seeing red squiggles in the editor?

Make sure the import path uses '../api' (two dots, going up one directory from components/ to src/).

Also confirm that api.js exists in the src/ folder (not inside components/).

help me fix import errors in my TicketList component

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

import { useState, useEffect } from 'react';
import { getTickets, deleteTicket, updateTicketStatus } from '../api';

function TicketList({ refreshKey, onRefresh }) {
  const [tickets, setTickets] = useState([]);

  useEffect(() => {
    getTickets().then(setTickets);
  }, [refreshKey]);

  const handleStatusChange = async (ticket) => {
    const nextStatus = ticket.status === 'Open' ? 'In Progress' : 'Resolved';
    await updateTicketStatus(ticket.id, nextStatus);
    onRefresh();
  };

  const handleDelete = async (id) => {
    await deleteTicket(id);
    onRefresh();
  };

  return (
    <div className="ticket-list">
      <h2>Support Tickets</h2>
      {tickets.length === 0 && <p>No tickets found.</p>}
      {tickets.map((ticket) => (
        <div key={ticket.id} className={`ticket-card status-${ticket.status.replace(' ', '-').toLowerCase()}`}>
          <div className="ticket-header">
            <h3>{ticket.title}</h3>
            <span className="status-badge">{ticket.status}</span>
          </div>
          <p>{ticket.description}</p>
          <div className="ticket-actions">
            {ticket.status !== 'Resolved' && (
              <button onClick={() => handleStatusChange(ticket)}>
                {ticket.status === 'Open' ? 'Start Progress' : 'Resolve'}
              </button>
            )}
            {ticket.status === 'Resolved' && (
              <button className="delete-btn" onClick={() => handleDelete(ticket.id)}>
                Delete
              </button>
            )}
          </div>
          <small>Created: {new Date(ticket.createdAt).toLocaleString()}</small>
        </div>
      ))}
    </div>
  );
}

export default TicketList;
Create the TicketForm component

The TicketForm component gives users a way to submit new support tickets. It captures a title and description, sends them to the backend via a POST request, then clears the form and signals the parent to refresh the ticket list.

  • Create a new file called TicketForm.jsx inside the helpdesk-client/src/components folder.
  • Add the following code to TicketForm.jsx:
import { useState } from 'react';
import { createTicket } from '../api';

function TicketForm({ onTicketCreated }) {
  // Track the form field values
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');

  // Submit the new ticket to the API
  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!title.trim() || !description.trim()) return;
    await createTicket({ title, description });
    setTitle('');
    setDescription('');
    onTicketCreated();
  };

  return (
    <form className="ticket-form" onSubmit={handleSubmit}>
      <h2>Submit a Ticket</h2>
      <input
        type="text"
        placeholder="Ticket title"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        required
      />
      <textarea
        placeholder="Describe your issue..."
        value={description}
        onChange={(e) => setDescription(e.target.value)}
        required
      />
      <button type="submit">Create Ticket</button>
    </form>
  );
}

export default TicketForm;

What does this code do?

  • Two useState hooks track the title and description field values as the user types.
  • handleSubmit prevents the default form reload, validates both fields are non-empty, then calls createTicket() from the API utility.
  • After a successful submission, the form resets both fields to empty strings and calls onTicketCreated() so the parent knows to refresh the ticket list.
  • Save TicketForm.jsx.
  • You won't see a change yet. The next substep wires both components into the App.

Editor showing an error on the import?

Confirm your file is named exactly TicketForm.jsx (capital T, capital F). Also check that the import path is '../api' with two dots, not one.

help me fix my TicketForm import path

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

import { useState } from 'react';
import { createTicket } from '../api';

function TicketForm({ onTicketCreated }) {
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!title.trim() || !description.trim()) return;
    await createTicket({ title, description });
    setTitle('');
    setDescription('');
    onTicketCreated();
  };

  return (
    <form className="ticket-form" onSubmit={handleSubmit}>
      <h2>Submit a Ticket</h2>
      <input
        type="text"
        placeholder="Ticket title"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        required
      />
      <textarea
        placeholder="Describe your issue..."
        value={description}
        onChange={(e) => setDescription(e.target.value)}
        required
      />
      <button type="submit">Create Ticket</button>
    </form>
  );
}

export default TicketForm;
Wire components into App

With both components ready, you need a parent that imports them, manages the shared refresh state, and provides the page layout. The App.jsx file will orchestrate everything.

  • Open helpdesk-client/src/App.jsx in the editor.
  • Replace all existing content with the following code:
import { useState } from 'react';
import TicketList from './components/TicketList';
import TicketForm from './components/TicketForm';
import './App.css';

function App() {
  // A counter that increments to signal child components to re-fetch
  const [refreshKey, setRefreshKey] = useState(0);

  // Increment refreshKey to trigger a ticket list refresh
  const handleRefresh = () => setRefreshKey((prev) => prev + 1);

  return (
    <div className="app">
      <header className="app-header">
        <h1>Helpdesk</h1>
        <p>IT Support Ticket System</p>
      </header>
      <main className="app-main">
        <TicketForm onTicketCreated={handleRefresh} />
        <TicketList refreshKey={refreshKey} onRefresh={handleRefresh} />
      </main>
    </div>
  );
}

export default App;

What does this code do?

  • refreshKey is a counter that acts as a coordination signal. When it increments, TicketList's useEffect fires and re-fetches the ticket data.
  • handleRefresh is passed to both components. TicketForm calls it after creating a ticket. TicketList calls it after updating or deleting one.
  • The layout uses a header for branding and a <main> element that will become a two-column grid once the CSS is added.
  • Save App.jsx.

Now add the stylesheet that gives the helpdesk its visual structure.

  • Open helpdesk-client/src/App.css in the editor.
  • Replace all existing content with the following styles:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background-color: #f5f5f5;
  color: #333;
}

.app {
  max-width: 900px;
  margin: 0 auto;
  padding: 20px;
}

.app-header {
  text-align: center;
  padding: 20px 0;
  border-bottom: 2px solid #007bff;
  margin-bottom: 30px;
}

.app-header h1 {
  color: #007bff;
  font-size: 2rem;
}

/* Two-column grid: form on left, ticket list on right */
.app-main {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: 30px;
}

/* Ticket creation form styles */
.ticket-form {
  background: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  height: fit-content;
}

.ticket-form h2 {
  margin-bottom: 15px;
  color: #333;
}

.ticket-form input,
.ticket-form textarea {
  width: 100%;
  padding: 10px;
  margin-bottom: 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
}

.ticket-form textarea {
  min-height: 100px;
  resize: vertical;
}

.ticket-form button {
  width: 100%;
  padding: 10px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.ticket-form button:hover {
  background: #0056b3;
}

/* Ticket list and card styles */
.ticket-list h2 {
  margin-bottom: 15px;
}

.ticket-card {
  background: white;
  padding: 15px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  margin-bottom: 12px;
  border-left: 4px solid #6c757d;
}

/* Color-coded left border based on ticket status */
.ticket-card.status-open {
  border-left-color: #007bff;
}

.ticket-card.status-in-progress {
  border-left-color: #ffc107;
}

.ticket-card.status-resolved {
  border-left-color: #28a745;
}

.ticket-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}

.ticket-header h3 {
  font-size: 1rem;
}

.status-badge {
  padding: 3px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: bold;
  background: #e9ecef;
}

.ticket-card p {
  color: #666;
  margin-bottom: 10px;
  font-size: 14px;
}

.ticket-actions {
  margin-bottom: 8px;
}

.ticket-actions button {
  padding: 5px 12px;
  margin-right: 8px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
  background: #007bff;
  color: white;
}

.ticket-actions button:hover {
  background: #0056b3;
}

.ticket-actions .delete-btn {
  background: #dc3545;
}

.ticket-actions .delete-btn:hover {
  background: #c82333;
}

.ticket-card small {
  color: #999;
  font-size: 12px;
}

What do these styles do?

The CSS creates a two-column grid layout. The form sits on the left (one fraction) and the ticket list takes up the right (two fractions).

Each ticket card gets a colored left border based on its status class: blue for Open, yellow for In Progress, and green for Resolved. This gives users an instant visual signal about ticket state.

  • Save App.css.
  • Make sure the backend is still running in Visual Studio (press F5 if you stopped it earlier).
  • In the integrated terminal, make sure you are inside the helpdesk-client folder, then start the frontend dev server by running:
npm run dev
  • Open your browser to http://localhost:5173. You should see the Helpdesk header, a ticket submission form on the left, and three seeded ticket cards on the right with blue and yellow left borders.

Seeing a blank page or no tickets?

  • Open the browser console (F12). If you see a CORS error, confirm that the backend is running on http://localhost:5000 and that Program.cs has the CORS policy allowing http://localhost:5173.
  • If you see a module import error, double-check that your import paths in App.jsx use './components/TicketList' and './components/TicketForm' (no file extension needed).
  • If the layout looks unstyled, confirm that App.jsx contains import './App.css' near the top.

help me debug my helpdesk React app not showing tickets

✔️ Awesome, I've got everything!

Great. Make sure both App.jsx and App.css are saved.

ⓧ I'd like to double check the full code

import { useState } from 'react';
import TicketList from './components/TicketList';
import TicketForm from './components/TicketForm';
import './App.css';

function App() {
  const [refreshKey, setRefreshKey] = useState(0);

  const handleRefresh = () => setRefreshKey((prev) => prev + 1);

  return (
    <div className="app">
      <header className="app-header">
        <h1>Helpdesk</h1>
        <p>IT Support Ticket System</p>
      </header>
      <main className="app-main">
        <TicketForm onTicketCreated={handleRefresh} />
        <TicketList refreshKey={refreshKey} onRefresh={handleRefresh} />
      </main>
    </div>
  );
}

export default App;
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background-color: #f5f5f5;
  color: #333;
}

.app {
  max-width: 900px;
  margin: 0 auto;
  padding: 20px;
}

.app-header {
  text-align: center;
  padding: 20px 0;
  border-bottom: 2px solid #007bff;
  margin-bottom: 30px;
}

.app-header h1 {
  color: #007bff;
  font-size: 2rem;
}

.app-main {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: 30px;
}

.ticket-form {
  background: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  height: fit-content;
}

.ticket-form h2 {
  margin-bottom: 15px;
  color: #333;
}

.ticket-form input,
.ticket-form textarea {
  width: 100%;
  padding: 10px;
  margin-bottom: 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
}

.ticket-form textarea {
  min-height: 100px;
  resize: vertical;
}

.ticket-form button {
  width: 100%;
  padding: 10px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.ticket-form button:hover {
  background: #0056b3;
}

.ticket-list h2 {
  margin-bottom: 15px;
}

.ticket-card {
  background: white;
  padding: 15px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  margin-bottom: 12px;
  border-left: 4px solid #6c757d;
}

.ticket-card.status-open {
  border-left-color: #007bff;
}

.ticket-card.status-in-progress {
  border-left-color: #ffc107;
}

.ticket-card.status-resolved {
  border-left-color: #28a745;
}

.ticket-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}

.ticket-header h3 {
  font-size: 1rem;
}

.status-badge {
  padding: 3px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: bold;
  background: #e9ecef;
}

.ticket-card p {
  color: #666;
  margin-bottom: 10px;
  font-size: 14px;
}

.ticket-actions {
  margin-bottom: 8px;
}

.ticket-actions button {
  padding: 5px 12px;
  margin-right: 8px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
  background: #007bff;
  color: white;
}

.ticket-actions button:hover {
  background: #0056b3;
}

.ticket-actions .delete-btn {
  background: #dc3545;
}

.ticket-actions .delete-btn:hover {
  background: #c82333;
}

.ticket-card small {
  color: #999;
  font-size: 12px;
}

Your frontend is displaying real ticket data from the backend. Next up, you'll add the ability to update ticket statuses and delete resolved tickets directly from the UI.

Add Ticket Status Updates and Deletion

Your components are built, your API is ready, and the frontend displays tickets. Now it's time to bring everything to life.

In this step, you'll trace through how the status update and delete logic in your TicketList component communicates with the backend. Then you'll run both servers simultaneously and verify the full ticket lifecycle.

In this step, get ready to:
  • Understand how ticket status progression works via PUT requests.
  • Understand how ticket deletion works for resolved tickets.
  • Run the full stack and verify the complete create-update-delete workflow.
Review the status update logic

A helpdesk ticket moves through a predictable lifecycle: Open, In Progress, then Resolved. Your TicketList component already handles this progression.

  • In Visual Studio 2026, open helpdesk-client/src/components/TicketList.jsx and find the handleStatusChange function:
const handleStatusChange = async (ticket) => {
  const nextStatus = ticket.status === 'Open' ? 'In Progress' : 'Resolved';
  await updateTicketStatus(ticket.id, nextStatus);
  onRefresh();
};

What does this code do?

This function determines the next status based on the current one. If a ticket is Open, the next status is In Progress. If it's already In Progress, the next status is Resolved.

It calls updateTicketStatus from your api.js module, which sends a PUT request to /api/tickets/{id} on the backend. After the update completes, onRefresh() increments the refreshKey in App.jsx, which triggers a fresh fetch of all tickets.

  • Now find the JSX that renders the status button inside the tickets.map() block:
{ticket.status !== 'Resolved' && (
  <button onClick={() => handleStatusChange(ticket)}>
    {ticket.status === 'Open' ? 'Start Progress' : 'Resolve'}
  </button>
)}

How does the button label change?

The button only appears when the ticket is not yet Resolved. Its label adapts based on the current status. When a ticket is Open, the button reads "Start Progress". When it's In Progress, it reads "Resolve".

Once a ticket reaches Resolved, the progress button disappears entirely. This prevents accidental status changes after resolution.

Review the delete logic

Deleting tickets should only be possible after they're resolved. You don't want someone accidentally removing an active issue from the queue.

  • In the same TicketList.jsx file, find the handleDelete function:
const handleDelete = async (id) => {
  await deleteTicket(id);
  onRefresh();
};
  • Now find the JSX that conditionally renders the delete button:
{ticket.status === 'Resolved' && (
  <button className="delete-btn" onClick={() => handleDelete(ticket.id)}>
    Delete
  </button>
)}

Why only delete resolved tickets?

The delete button only renders when ticket.status === 'Resolved'. This is a deliberate design choice. In a real helpdesk, you'd never want to delete an open or in-progress ticket because someone might still be working on it.

The deleteTicket function sends a DELETE request to /api/tickets/{id} on your backend, which removes the ticket from the in-memory database permanently.

Run the full stack and verify the workflow

Both your backend and frontend need to run simultaneously. The backend serves the API on port 5000, and the React dev server runs on port 5173.

  • In Visual Studio 2026, press F5 to start the backend. The Swagger UI opens in your browser automatically.
  • Return to the terminal in Visual Studio (View > Terminal).
  • Navigate to the frontend folder and start the dev server by running:
cd helpdesk-client
npm run dev

What does this command do?

The npm run dev command starts the Vite development server. It serves your React app on http://localhost:5173 and automatically reloads when you save file changes.

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

You should see the Helpdesk header, the ticket submission form on the left, and the three seeded tickets on the right.

  • Test status updates by clicking the Start Progress button on one of the Open tickets.
  • Confirm the ticket's left border changes from blue to yellow, and the badge text updates to "In Progress".
  • Click the Resolve button on the same ticket.
  • Confirm the left border changes to green, the badge reads "Resolved", and a red Delete button appears.
  • Click Delete and confirm the ticket disappears from the list.
  • Test ticket creation by entering a title and description in the form, then clicking Create Ticket.
  • Confirm the new ticket appears at the top of the list with an "Open" status badge.

Tickets not loading or buttons not working?

  • Make sure the backend is still running in Visual Studio. If you stopped it, press F5 again.
  • Check the browser console (F12 > Console tab) for CORS errors. If you see one, verify that http://localhost:5173 is in the WithOrigins call in Program.cs.
  • If the API returns 404, make sure the backend's port matches API_BASE in api.js (http://localhost:5000/api).

Need more help? Why aren't my API calls connecting to the backend?

Your full-stack helpdesk application is working end to end. You've connected a React frontend to an ASP.NET Core backend and verified the complete ticket lifecycle. Next up, a secret mission to add priority levels and filtering.

Secret mission

Add Priority and Filtering

Your helpdesk works, but every ticket looks equally urgent. In a real support system, a server outage needs attention before a font preference request. Can you add a priority system with color-coded badges and filtering so Critical tickets always rise to the top?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Your in-memory database resets every time you stop the backend, and no cloud services were provisioned.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • HelpdeskApi project folder (ASP.NET Core backend).
  • helpdesk-client project folder (React frontend).
  • Visual Studio 2026 Community (installed application with .NET 10 SDK and Node.js).

Keep everything running

No action needed. Choose this if you're still actively building or plan to extend the helpdesk system further.

  • Your project files stay on disk wherever Visual Studio created them.
  • The in-memory database resets each time you restart the backend, so no stale data accumulates.
  • Visual Studio 2026 Community, .NET 10 SDK, and Node.js remain installed for future projects.

Pause - I'll come back to this later

Stop any running processes to free up system memory. Your files stay intact for when you return.

  • In Visual Studio, press Shift+F5 to stop the backend if it is still running.
  • In the terminal running the React dev server, press Ctrl+C to stop the Vite process.
  • Close Visual Studio. Your solution file remembers which files were open.

Delete - I don't want to use this again

Remove all project files and optionally uninstall the development tools.

  • Stop the backend and frontend processes if they are still running (Shift+F5 in Visual Studio, Ctrl+C in the terminal).
  • Close Visual Studio completely.
  • Open File Explorer and navigate to the folder where Visual Studio created your HelpdeskApi solution.
  • Delete the entire solution folder (this contains both HelpdeskApi and helpdesk-client).
  • To uninstall Visual Studio 2026 Community, open Settings, select Apps, then Installed apps. Find Visual Studio Community 2026 and click Uninstall. This also removes the .NET 10 SDK and Node.js that were installed via the workloads.

Nice Work!

Nice Work!

You just built a full-stack helpdesk application from scratch. You now have a working ASP.NET Core backend wired to a React frontend, proving you can connect a .NET API to a modern JavaScript SPA.

You've learned how to:

  • Built an ASP.NET Core Web API with Entity Framework Core and an in-memory database to manage helpdesk tickets through REST endpoints.
  • Created a React SPA scaffolded with Vite that fetches, displays, creates, updates, and deletes tickets by communicating with your backend.
  • Configured CORS to allow your frontend and backend to communicate during local development, the foundational pattern for full-stack .NET applications.
  • Added a priority system with color-coded badges and filtering so users can sort tickets by urgency.

Ready to quiz yourself?