{"id":57395,"date":"2025-07-30T10:00:00","date_gmt":"2025-07-30T17:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=57395"},"modified":"2025-07-30T12:44:18","modified_gmt":"2025-07-30T19:44:18","slug":"new-aspire-app-with-react","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/new-aspire-app-with-react\/","title":{"rendered":"Building a Full-Stack App with React and Aspire: A Step-by-Step Guide"},"content":{"rendered":"<p>In this post we will build we will build a TODO app from start to finish using Aspire and React.\nWe will do this using the CLI and C# Dev Kit. The todo items will be stored in a SQLite database.\nThe React front-end will use a Web API to handle all the interactions with the data.\nI&#8217;m going to be showing this with the dotnet CLI, Aspire CLI and C# Dev Kit, but you can follow along with any IDE, or editor, of your choice.\nThe resulting app can be published to any web host which supports ASP.NET Core &#8211; including Linux containers.\nFirst let\u2019s start with the prerequisites to ensure you have all the components needed to follow along this tutorial.<\/p>\n<p><div class=\"alert alert-success\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Lightbulb\"><\/i><strong>Source Code<\/strong><\/p>All the code from this post can be found at <a href=\"https:\/\/github.com\/sayedihashimi\/todojsaspire\">sayedihashimi\/todojsaspire<\/a>.<\/div><\/p>\n<h2>Prerequisites<\/h2>\n<p>In this tutorial, we will walk through installing Aspire, but you should have these dependencies installed. You can learn more at <a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/get-started\/build-your-first-aspire-app?pivots=vscode#prerequisites\">Aspire Prerequisites<\/a> Installing these items will not be covered in this post.<\/p>\n<ul>\n<li><a href=\"https:\/\/dotnet.microsoft.com\/download\/dotnet\/9.0\">.NET 9.0<\/a><\/li>\n<li><a href=\"https:\/\/nodejs.org\/en\/download\">nodejs<\/a><\/li>\n<li>VS Code with <a href=\"https:\/\/marketplace.visualstudio.com\/items?itemName=ms-dotnettools.csdevkit\">C# Dev Kit<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/fundamentals\/setup-tooling?tabs=windows&amp;pivots=vscode#container-runtime\">Container runtime<\/a><\/li>\n<\/ul>\n<h2>Install Aspire<\/h2>\n<p>For detailed instructions on getting Aspire, and its dependencies, installed visit\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/fundamentals\/setup-tooling?tabs=windows&amp;pivots=vscode\">Aspire setup and tooling<\/a>. We will go through the basics here.\nAfter installing .NET 9 and the other dependencies we will install the project templates using <code>dotnet new<\/code>.<\/p>\n<p><div class=\"alert alert-info\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Workload Migration<\/strong><\/p>As of version 9, Aspire no longer requires a separate workload installation. Use <code>dotnet workload list<\/code> to check installed workloads and <code>dotnet workload uninstall<\/code> to remove the Aspire workload.<\/div><\/p>\n<p>Install the new <a href=\"https:\/\/www.nuget.org\/packages\/Aspire.Cli\">Aspire CLI<\/a>. The command below will install the tool globally and the <code>dotnet new<\/code> templates.<\/p>\n<p>On Windows:<\/p>\n<pre><code class=\"language-powershell\">iex \"&amp; { $(irm https:\/\/aspire.dev\/install.ps1) }\"<\/code><\/pre>\n<p>On Linux, or macOS:<\/p>\n<pre><code class=\"language-sh\">curl -sSL https:\/\/aspire.dev\/install.sh | bash -s<\/code><\/pre>\n<p>After installing this tool, you can run it by executing <code>aspire<\/code> on the command line. You can explore the usage of this tool with <code>aspire -\u2013help<\/code>. Now that we have the tools installed, let\u2019s move on and create the Aspire app.<\/p>\n<h2>Create the Aspire app<\/h2>\n<p>Now that the machine is ready with all the prerequisites we can get started.\nOpen an empty folder in VS Code and add a new directory named <code>src<\/code> for the source files.<\/p>\n<p>Let\u2019s create the Aspire app to start with. In VS Code open the command palette\n<code>CTRL\/CMD-SHIFT-P<\/code> and type in New Project. See the following image.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/07\/01-command-palette-new-project.png\" alt=\"VS Code command palette showing the New Project option highlighted\" \/><\/p>\n<p>Select the Aspire Starter App template and hit enter.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/07\/02-command-palette-new-aspire-starter-app.png\" alt=\"VS Code command palette showing the Aspire Starter App template selection\" \/><\/p>\n<p>When prompted for the project name use &#8220;TodojsAspire&#8221; and select &#8220;src&#8221; as the destination folder to follow along.\nI will walk you through using New Project to create the Aspire app in the video below. Alternatively, you can use <code>dotnet new aspire-starter<\/code> or <code>aspire new aspire-starter<\/code>\nin a terminal for the same result.<\/p>\n<p><div style=\"width: 640px;\" class=\"wp-video\"><video class=\"wp-video-shortcode\" id=\"video-57395-1\" width=\"640\" height=\"360\" preload=\"metadata\" controls=\"controls\"><source type=\"video\/mp4\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/06\/03-todojsaspire-create-aspire-app.mp4?_=1\" \/><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/06\/03-todojsaspire-create-aspire-app.mp4\">https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/06\/03-todojsaspire-create-aspire-app.mp4<\/a><\/video><\/div><\/p>\n<p>Now that the starter app has been created you should see the following in the Explorer in VS Code. In this case I added the following files before creating the project <code>.gitattributes<\/code>, <code>.gitignore<\/code> and <code>LICENSE<\/code>.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/07\/04-explorer-with-aspire-app.png\" alt=\"VS Code Explorer panel showing the folder structure of the newly created Aspire app\" \/><\/p>\n<p>Now would be a good time to execute a build to ensure that there are no build issues.\nOpen the command palette with <code>CTRL\/CMD-SHIFT-P<\/code> and select &#8220;.NET: Build&#8221;. You can also use the Solution Explorer to perform the build if you prefer that method.<\/p>\n<p>When using the Aspire Starter App template it will create a few projects including a front-end with ASP.NET Core. Since we are going to use React for the front-end,\nwe can delete the TodojsAspire.Web project and remove any references to it in the remaining files. The easiest way to do this project is to use the Solution Explorer which\ncomes with C# Dev Kit. After opening the Solution Explorer, right click on the TodojsAspire.Web project and select Remove. See the following image.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/07\/05-solution-explorer-remove-web-project.png\" alt=\"Solution Explorer in VS Code showing the context menu with Delete option for removing the TodojsAspire.Web project\" \/><\/p>\n<p>After deleting the project we need to remove any references to it. The things that need to be removed include.<\/p>\n<ul>\n<li>Project reference in TodojsAspire.AppHost<\/li>\n<li>Update AppHost in TodojsAspire.AppHost<\/li>\n<\/ul>\n<p>In the command palette you can use .NET: Remove Project Reference to delete the reference in TodojsAspire.AppHost. Then delete the following code from the AppHost.cs file in the\nsame project.<\/p>\n<pre><code class=\"language-csharp\">builder.AddProject&lt;Projects.TodojsAspire_Web&gt;(\"webfrontend\")\r\n    .WithExternalHttpEndpoints()\r\n    .WithHttpHealthCheck(\"\/health\")\r\n    .WithReference(apiService)\r\n    .WaitFor(apiService);<\/code><\/pre>\n<p>Soon we will replace these lines with what is needed to integrate the React app.\nYou should also delete the TodojsAspire.Web folder from the src directory.\nAfter making those changes, you should do a build to ensure that nothing was missed. To start a\nbuild, open the command palette and select <code>Task: Run Build Task<\/code> and then select <code>dotnet: build<\/code>.\nNow that we have cleaned up the solution, we will move on to start updating the API project to expose endpoints to manage\nthe TODO items.<\/p>\n<h2>Configure the Web API<\/h2>\n<p>To get the API project going, we will first add a model class for the TODO items, and then use <code>dotnet scaffold<\/code> to generate the initial API endpoints.\nAdd the Todo class (<code>Todo.cs<\/code>) below to the TodojsAspire.ApiService project.<\/p>\n<pre><code class=\"language-csharp\">using System.ComponentModel.DataAnnotations;\r\nnamespace TodojsAspire.ApiService;\r\n\r\npublic class Todo\r\n{\r\n    public int Id { get; set; }\r\n    [Required]\r\n    public string Title { get; set; } = default!;\r\n    public bool IsComplete { get; set; } = false;\r\n    \/\/ The position of the todo in the list, used for ordering.\r\n    \/\/ When updating this, make sure to not duplicate values.\r\n    \/\/ To move an item up\/down, swap the values of the position\r\n    [Required]\r\n    public int Position { get; set; } = 0;\r\n}<\/code><\/pre>\n<p>Now that we have added the model class, we will scaffold the API endpoints with <code>dotnet scaffold<\/code>.<\/p>\n<p>We can use <code>dotnet scaffold<\/code> to generate API endpoints for the Todo model. To install this tool, execute the following command.<\/p>\n<pre><code class=\"language-bash\">dotnet tool install --global Microsoft.dotnet-scaffold<\/code><\/pre>\n<p>When using <code>dotnet scaffold<\/code> it\u2019s easiest to <code>cd<\/code> into the project directory and then execute it from there. This tool is interactive by default, to get started execute <code>dotnet scaffold<\/code>. Make the following selections.<\/p>\n<ul>\n<li>Category = API<\/li>\n<li>Command = Minimal API<\/li>\n<li>Project = TodojsAspire.ApiService<\/li>\n<li>Model = Todo<\/li>\n<li>Endpoints file name = TodoEndpoints<\/li>\n<li>Open API Enabled = No<\/li>\n<li>Data context class = TodoDbContext<\/li>\n<li>Database provider = sqlite-efcore<\/li>\n<li>Include prerelease = No<\/li>\n<\/ul>\n<p>You can see the entire interaction in the following animation.<\/p>\n<p><div style=\"width: 640px;\" class=\"wp-video\"><video class=\"wp-video-shortcode\" id=\"video-57395-2\" width=\"640\" height=\"360\" preload=\"metadata\" controls=\"controls\"><source type=\"video\/mp4\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/06\/06-dotnet-scaffold-todoapi.mp4?_=2\" \/><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/06\/06-dotnet-scaffold-todoapi.mp4\">https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/06\/06-dotnet-scaffold-todoapi.mp4<\/a><\/video><\/div><\/p>\n<p>The following changes were made to the TodojsAspire.ApiService project.<\/p>\n<ul>\n<li>TodoEndpoints.cs file was created with the Minimal API endpoints.<\/li>\n<li>Program.cs was modified to; initialize the SQLite database, get the connection string from appsettings.json and to a call to map the endpoints in TodoEndpoints.<\/li>\n<li>The project file was modified to add needed NuGet packages.<\/li>\n<li>appsettings.json was modified to add the connection to the local db file.<\/li>\n<\/ul>\n<p>Kick off another build to ensure that scaffolding has worked successfully. If you get any build errors regarding missing packages, ensure that the following packages have been installed.<\/p>\n<ul>\n<li>Microsoft.EntityFrameworkCore<\/li>\n<li>Microsoft.EntityFrameworkCore.Design<\/li>\n<li>Microsoft.EntityFrameworkCore.Sqlite<\/li>\n<li>Microsoft.EntityFrameworkCore.Tools<\/li>\n<li>System.ComponentModel.Annotations<\/li>\n<\/ul>\n<p>You can install packages using <code>dotnet add package [PACKAGE NAME]<\/code>.<\/p>\n<p>Open the new file <code>TodoEndpoints.cs<\/code> so that we can take a look.\nSince this is a simple app, we can simplify the URL to the API. When you have the TodoEndpoints.cs class open in VS Code, use Replace all to replace <code>\/api\/<\/code> with <code>\/<\/code>.\nThe resulting class, TodoEndpoints.cs, is below.<\/p>\n<pre><code class=\"language-csharp\">using Microsoft.AspNetCore.Http.HttpResults;\r\nusing Microsoft.EntityFrameworkCore;\r\nusing TodojsAspire.ApiService;\r\n\r\npublic static class TodoEndpoints\r\n{\r\n    public static void MapTodoEndpoints(this IEndpointRouteBuilder routes)\r\n    {\r\n        var group = routes.MapGroup(\"\/Todo\");\r\n\r\n        group.MapGet(\"\/\", async (TodoDbContext db) =&gt;\r\n        {\r\n            return await db.Todo.ToListAsync();\r\n        })\r\n        .WithName(\"GetAllTodos\");\r\n\r\n        group.MapGet(\"\/{id}\", async Task&lt;Results&lt;Ok&lt;Todo&gt;, NotFound&gt;&gt; (int id, TodoDbContext db) =&gt;\r\n        {\r\n            return await db.Todo.AsNoTracking()\r\n                .FirstOrDefaultAsync(model =&gt; model.Id == id)\r\n                is Todo model\r\n                    ? TypedResults.Ok(model)\r\n                    : TypedResults.NotFound();\r\n        })\r\n        .WithName(\"GetTodoById\");\r\n\r\n        group.MapPut(\"\/{id}\", async Task&lt;Results&lt;Ok, NotFound&gt;&gt; (int id, Todo todo, TodoDbContext db) =&gt;\r\n        {\r\n            var affected = await db.Todo\r\n                .Where(model =&gt; model.Id == id)\r\n                .ExecuteUpdateAsync(setters =&gt; setters\r\n                .SetProperty(m =&gt; m.Title, todo.Title)\r\n                .SetProperty(m =&gt; m.IsComplete, todo.IsComplete)\r\n                .SetProperty(m =&gt; m.Position, todo.Position)\r\n        );\r\n\r\n            return affected == 1 ? TypedResults.Ok() : TypedResults.NotFound();\r\n        })\r\n        .WithName(\"UpdateTodo\");\r\n\r\n        group.MapPost(\"\/\", async (Todo todo, TodoDbContext db) =&gt;\r\n        {\r\n            db.Todo.Add(todo);\r\n            await db.SaveChangesAsync();\r\n            return TypedResults.Created($\"\/Todo\/{todo.Id}\",todo);\r\n        })\r\n        .WithName(\"CreateTodo\");\r\n\r\n        group.MapDelete(\"\/{id}\", async Task&lt;Results&lt;Ok, NotFound&gt;&gt; (int id, TodoDbContext db) =&gt;\r\n        {\r\n            var affected = await db.Todo\r\n                .Where(model =&gt; model.Id == id)\r\n                .ExecuteDeleteAsync();\r\n\r\n            return affected == 1 ? TypedResults.Ok() : TypedResults.NotFound();\r\n        })\r\n        .WithName(\"DeleteTodo\");\r\n    }\r\n}<\/code><\/pre>\n<p>This file contains the CRUD methods which are needed to support reading\/writing the content from the database.\nIn the front-end that we will create soon, we want to give the user the ability to move tasks up\/down in the list. There are lots of different ways to implement this. Since this is\na simple todo app for a single user, we don\u2019t need to worry about having a large number of items. To keep it simple, we will add two new endpoints; <code>MoveTaskUp<\/code> and <code>MoveTaskDown<\/code>.\nThe code for these endpoints are below, add it below the last endpoint in the TodoEndpoints class.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Endpoint to move a task up in the list\r\ngroup.MapPost(\"\/move-up\/{id:int}\", async Task&lt;Results&lt;Ok, NotFound&gt;&gt; (int id, TodoDbContext db) =&gt;\r\n{\r\n    var todo = await db.Todo.FirstOrDefaultAsync(t =&gt; t.Id == id);\r\n    if (todo is null)\r\n    { return TypedResults.NotFound(); }\r\n\r\n    \/\/ Find the todo with the largest position less than the current todo\r\n    var prevTodo = await db.Todo\r\n        .Where(t =&gt; t.Position &lt; todo.Position)\r\n        .OrderByDescending(t =&gt; t.Position)\r\n        .FirstOrDefaultAsync();\r\n\r\n    if (prevTodo is null)\r\n    { return TypedResults.Ok(); }\r\n\r\n    \/\/ Swap positions\r\n    (todo.Position, prevTodo.Position) = (prevTodo.Position, todo.Position);\r\n    await db.SaveChangesAsync();\r\n    return TypedResults.Ok();\r\n})\r\n.WithName(\"MoveTaskUp\");\r\n\r\n\/\/ Endpoint to move a task down in the list\r\ngroup.MapPost(\"\/move-down\/{id:int}\", async Task&lt;Results&lt;Ok, NotFound&gt;&gt; (int id, TodoDbContext db) =&gt;\r\n{\r\n    var todo = await db.Todo.FirstOrDefaultAsync(t =&gt; t.Id == id);\r\n    if (todo is null)\r\n    { return TypedResults.NotFound(); }\r\n\r\n    \/\/ Find the todo with the smallest position greater than the current todo\r\n    var nextTodo = await db.Todo\r\n        .Where(t =&gt; t.Position &gt; todo.Position)\r\n        .OrderBy(t =&gt; t.Position)\r\n        .FirstOrDefaultAsync();\r\n\r\n    if (nextTodo is null)\r\n    { return TypedResults.Ok(); } \/\/ Already at the bottom or no next todo\r\n\r\n    \/\/ Swap positions values\r\n    (todo.Position, nextTodo.Position) = (nextTodo.Position, todo.Position);\r\n    await db.SaveChangesAsync();\r\n    return TypedResults.Ok();\r\n})\r\n.WithName(\"MoveTaskDown\");<\/code><\/pre>\n<p><code>MoveTaskUp<\/code> will find the task with a next lower position, and then swaps the position values. This line of code\n<code>(todo.Position, prevTodo.Position) = (prevTodo.Position, todo.Position);<\/code> uses tuple assignment to swap the position values in a single line of code.<\/p>\n<h2>Configure the database<\/h2>\n<p>Now that we have all the database related code ready, we need to create an <a href=\"https:\/\/learn.microsoft.com\/ef\/core\/managing-schemas\/migrations\/?tabs=dotnet-core-cli\">EF migration<\/a>. After we create the migration we will integrate the database with the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/fundamentals\/dashboard\/overview?tabs=bash\">Aspire dashboard<\/a>.<\/p>\n<p>To create the EF migration, open the terminal in VS Code,\n<code>cd<\/code> into the TodojsAspire.ApiService project directory (<code>src\/TodojsAspire.ApiService<\/code>). Then execute the following command.<\/p>\n<ul>\n<li><code>dotnet ef migrations add TodoEndpointsInitialCreate<\/code><\/li>\n<\/ul>\n<p>The migrations command will generate a new migration named <code>TodoEndpointsInitialCreate<\/code> and add it to the project. At this time you would typically also run <code>dotnet ef database update<\/code> but that isn&#8217;t needed in this case.\nWe will configure the project to run migrations when it is started by the AppHost.\nLet&#8217;s configure the database in the AppHost now.<\/p>\n<p>For SQLite support in the AppHost, we will need to use the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/community-toolkit\/overview\">Aspire Community Toolkit<\/a>. Execute the command below in the &#8220;src&#8221; folder to install SQLite support in the AppHost.<\/p>\n<pre><code class=\"language-bash\">aspire add sqlite<\/code><\/pre>\n<p>Follow the prompts to add the package. This will add a PackageReference to the AppHost and make other APIs available for the builder.<\/p>\n<p>Open the AppHost.cs file in the TodojsAspire.AppHost project. Replace the contents with the code below.<\/p>\n<pre><code class=\"language-csharp\">var builder = DistributedApplication.CreateBuilder(args);\r\n\r\nvar db = builder.AddSqlite(\"db\")\r\n    .WithSqliteWeb();\r\n\r\nvar apiService = builder.AddProject&lt;Projects.TodojsAspire_ApiService&gt;(\"apiservice\")\r\n    .WithReference(db)\r\n    .WithHttpHealthCheck(\"\/health\");\r\n\r\nbuilder.Build().Run();<\/code><\/pre>\n<p>In AppHost.cs we have added a SQLite database and registered the API service. We called <code>WithReference(db)<\/code> on the API so that it gets the connection\nstring to the database.<\/p>\n<p>To configure the ApiService we will need to add the package <code>CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite<\/code> and update the connection\nto the database. In a terminal first <code>cd<\/code> into the ApiService project and execute the command below.<\/p>\n<pre><code class=\"language-bash\">dotnet add package CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite<\/code><\/pre>\n<p>Modify the <code>Program.cs<\/code> in the Api project to have the following contents.<\/p>\n<pre><code class=\"language-cs\">using Microsoft.EntityFrameworkCore;\r\n\r\nvar builder = WebApplication.CreateBuilder(args);\r\n\r\nbuilder.AddSqliteDbContext&lt;TodoDbContext&gt;(\"db\");\r\n\r\n\/\/ Add service defaults &amp; Aspire client integrations.\r\nbuilder.AddServiceDefaults();\r\n\r\n\/\/ Add services to the container.\r\nbuilder.Services.AddProblemDetails();\r\n\r\n\/\/ Learn more about configuring OpenAPI at https:\/\/aka.ms\/aspnet\/openapi\r\nbuilder.Services.AddOpenApi();\r\n\r\nvar app = builder.Build();\r\n\r\n\/\/ Configure the HTTP request pipeline.\r\napp.UseExceptionHandler();\r\n\r\nif (app.Environment.IsDevelopment())\r\n{\r\n    app.MapOpenApi();\r\n}\r\n\r\napp.MapDefaultEndpoints();\r\n\r\napp.MapTodoEndpoints();\r\n\r\nusing var scope = app.Services.CreateScope();\r\nvar dbContext = scope.ServiceProvider.GetRequiredService&lt;TodoDbContext&gt;();\r\nawait dbContext.Database.MigrateAsync();\r\n\r\napp.Run();<\/code><\/pre>\n<p>The most important changes here are that we changed how the database is being initalized. Previously the connection string was coming from the\nappsettings.json file from the API project, it&#8217;s now being injected with <code>builder.AddSqliteDbContext&lt;TodoDbContext&gt;(\"db\")<\/code>. You should remove the\nconnection string from the appsettings.json file now.\nAt the bottom of Program.cs we have added <code>await dbContext.Database.MigrateAsync()<\/code> to ensure that the database is up-to-date when\nthe AppHost starts the API project. We will now move on to try out the Web API to ensure there are no issues.<\/p>\n<h2>Exercise the API to ensure it&#8217;s working as expected<\/h2>\n<p>Now that we have all the\nendpoints that we need, it\u2019s time to test this out. To test this we will add an HTTP file. For HTTP file support in VS Code, you\u2019ll need to add an extension. There are several\nthat you can pick from, including <a href=\"https:\/\/marketplace.visualstudio.com\/items?itemName=humao.rest-client\">REST Client<\/a> and\n<a href=\"https:\/\/marketplace.visualstudio.com\/items?itemName=anweber.vscode-httpyac\">httpYac<\/a>. Either of those will work for our needs. For this tutorial, I\u2019ll show it with the\nREST Client, but the experience with httpYac is very similar and you should be able to follow along. To install that use the Extensions tab in VS Code and type in \u201cREST Client\u201d\nin the search box, then click Install. See the next image.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/07\/07-install-rest-client-extension.png\" alt=\"VS Code Extensions panel showing the REST Client extension ready for installation\" \/><\/p>\n<p>In the TodojsAspire.ApiService project open the file named TodojsAspire.ApiService.http. If your project doesn&#8217;t have a file with that name, create a new one.\nThe name of the HTTP file doesn\u2019t matter; you can name it whatever you like.\nBefore we start writing any requests in the HTTP file, run the app. To start the app, you have a few options when using C# Dev Kit. You can use the Run and Debug tab in VS Code;\nyou can use Start Debugging (<code>F5<\/code>) or Start without Debugging (<code>CTRL-F5<\/code>). In this case we don\u2019t need to debug so we can use the keyboard shortcut <code>CTRL-F5<\/code> to Start without Debugging and\nchoose <code>App Host [default configuration]<\/code>.\nYou should have a .cs file opened in the VS Code editor when invoking that gesture. That will ensure that you get the right options from VS Code. When you are prompted to select\nthe launch configuration, choose the AppHost project. This will start the Aspire Dashboard and it will automatically startup the ApiService as well.<\/p>\n<p><div style=\"width: 640px;\" class=\"wp-video\"><video class=\"wp-video-shortcode\" id=\"video-57395-3\" width=\"640\" height=\"360\" preload=\"metadata\" controls=\"controls\"><source type=\"video\/mp4\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/06\/08-aspire-ctrl-f5.mp4?_=3\" \/><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/06\/08-aspire-ctrl-f5.mp4\">https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/06\/08-aspire-ctrl-f5.mp4<\/a><\/video><\/div><\/p>\n<p>For detailed info on the dashboard, see this article\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/fundamentals\/dashboard\/overview?tabs=bash\">Aspire dashboard overview &#8211; Aspire | Microsoft Learn<\/a>. We will go over the\nbasics here. In the Aspire dashboard. Below I\u2019ve copied the key features from the dashboard article.<\/p>\n<p>Key features of the dashboard include:<\/p>\n<ul>\n<li>Real-time tracking of logs, traces, and environment configurations.<\/li>\n<li>User interface to <a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/fundamentals\/dashboard\/explore#resource-actions\">stop, start, and restart resources<\/a>.<\/li>\n<li>Collects and displays logs and telemetry; <a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/fundamentals\/dashboard\/explore#monitoring-pages\">view structured logs, traces, and metrics<\/a> in an intuitive UI.<\/li>\n<li>Enhanced debugging with <a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/fundamentals\/dashboard\/copilot\">GitHub Copilot<\/a>, your AI-powered assistant built into the dashboard.<\/li>\n<\/ul>\n<p>The dashboard will show the projects which have been configured and their status. You can easily navigate to the app, view logs and other important info. This dashboard currently\nshows the ApiService project, the SQLite database and a web interface to interact with the database. Later when we add the React app, it will appear in the dashboard as well. See the screenshot below.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/07\/09-aspire-dashboard.png\" alt=\"Aspire dashboard showing the ApiService and SQLite database components with their status and endpoints\" \/><\/p>\n<p>In the screenshot above, you can see the URLs for the ApiService project. Copy one of the URLs for the ApiService project, we will need that to exercise the app. You can click on the URL for <code>db-sqliteweb<\/code> to open a web interface to interact with the database, but that isn&#8217;t needed for this tutorial.<\/p>\n<p>By default, when you start the AppHost, you will get a new database and the migration(s) will automatically be applied to the database to update it. If you want your local data to persist\nyou can override this in AppHost by specifying a specific connection string to be used. Now let\u2019s move on to create an HTTP file to ensure that the endpoints work as expected.<\/p>\n<p>Below is the HTTP file, you may need to update the base url variable on the first line to match your project. For more info on HTTP file see the\n<a href=\"https:\/\/marketplace.visualstudio.com\/items?itemName=humao.rest-client\">REST Client documentation<\/a> or\n<a href=\"https:\/\/learn.microsoft.com\/aspnet\/core\/test\/http-files?view=aspnetcore-9.0\">Use .http files in Visual Studio 2022 | Microsoft Learn<\/a>\n(<em>note: some of the features described aren\u2019t supported outside of Visual Studio 2022<\/em>).<\/p>\n<pre><code class=\"language-text\">@todoapibaseurl = https:\/\/localhost:7473\r\n\r\nGET {{todoapibaseurl}}\/Todo\/\r\n\r\n###\r\n\r\n# Create a new todo\r\nPOST {{todoapibaseurl}}\/Todo\/\r\nContent-Type: application\/json\r\n\r\n{\r\n  \"title\": \"Sample Todo2\",\r\n  \"isComplete\": false,\r\n  \"position\": 1\r\n}\r\n\r\n###\r\nPOST {{todoapibaseurl}}\/Todo\/\r\nContent-Type: application\/json\r\n\r\n{\r\n  \"title\": \"Sample Todo2\",\r\n  \"isComplete\": false,\r\n  \"position\": 2\r\n}\r\n###\r\nPOST {{todoapibaseurl}}\/Todo\/\r\nContent-Type: application\/json\r\n\r\n{\r\n  \"title\": \"Sample Todo3\",\r\n  \"isComplete\": false,\r\n  \"position\": 3\r\n}\r\n\r\n###\r\nPUT {{todoapibaseurl}}\/Todo\/1\r\nContent-Type: application\/json\r\n\r\n{\r\n  \"id\": 1,\r\n  \"title\": \"Updated Todo\",\r\n  \"isComplete\": true,\r\n  \"position\": 20\r\n}\r\n\r\n###\r\n\r\nPOST {{todoapibaseurl}}\/Todo\/\r\nContent-Type: application\/json\r\n\r\n{\r\n  \"title\": \"Sample Todo no position\",\r\n  \"isComplete\": false\r\n}\r\n###\r\n\r\n# Delete a todo\r\nDELETE {{todoapibaseurl}}\/Todo\/1\r\n\r\n###\r\n\r\nPOST {{todoapibaseurl}}\/Todo\/move-up\/3\r\n###<\/code><\/pre>\n<p>When you paste the value for the API URL make sure to remove the trailing slash.<\/p>\n<p>With this HTTP file we can exercise the app. It includes requests for most endpoints in the TodoEndpoints class. You can execute the requests with <code>Send Request<\/code> above the URL line.\nYou can also use <code>Rest Client: Send Request<\/code> in the command palette.\nTry out the different requests to make sure things are working correctly.\nRemember that the database will be wiped out when the app is restarted, so you\ndon&#8217;t need to worry about adding this data. When working with this file I noticed\ntwo issues what should be addressed.<\/p>\n<ul>\n<li>When Todo items are returned, they are not sorted by Position.<\/li>\n<li>When a Todo item is POSTed without a position, the value for position will be assigned to 0.<\/li>\n<\/ul>\n<p>To fix the first issue, specifically <code>group.MapGet(\"\/\",<\/code>, update the get endpoint to have the following code.<\/p>\n<pre><code class=\"language-csharp\">group.MapGet(\"\/\", async (TodoDbContext db) =&gt;\r\n{\r\n    return await db.Todo.OrderBy(t =&gt; t.Position).ToListAsync();\r\n})\r\n.WithName(\"GetAllTodos\");<\/code><\/pre>\n<p>To fix the issue regarding the missing position value, update the POST method to have the following code.<\/p>\n<pre><code class=\"language-csharp\">group.MapPost(\"\/\", async (Todo todo, TodoDbContext db) =&gt;\r\n{\r\n    if (todo.Position &lt;= 0)\r\n    {\r\n        \/\/ If position is not set, assign it to the next available position\r\n        todo.Position = await db.Todo.AnyAsync()\r\n            ? await db.Todo.MaxAsync(t =&gt; t.Position) + 1\r\n            : 1; \/\/ Start at position 1 if no todos exist\r\n    }\r\n    db.Todo.Add(todo);\r\n    await db.SaveChangesAsync();\r\n    return TypedResults.Created($\"\/Todo\/{todo.Id}\", todo);\r\n})\r\n.WithName(\"CreateTodo\");<\/code><\/pre>\n<p>With this change, when a Todo item is submitted without a value for Position, the value for Position will be set to the max value of Position in the database + 1. Now we have\neverything that we need for the API, we will move on to start the JS front-end.<\/p>\n<h2>Build the React front-end<\/h2>\n<p>To create the React project we will use the <code>npm<\/code> command which is installed with node. Visit <a href=\"https:\/\/nodejs.org\/en\/download\">Node.js \u2014 Download Node.js\u00ae<\/a> to get it installed.\nWe will use <a href=\"https:\/\/vite.dev\/\">vite<\/a> as the front-end build tool.<\/p>\n<p>Open a terminal, <code>cd<\/code> into the src directory and then execute the command below.<\/p>\n<pre><code class=\"language-bash\">npm create vite@latest todo-frontend -- --template react<\/code><\/pre>\n<p>When prompted specify the following values.<\/p>\n<ul>\n<li>Framework = React<\/li>\n<li>Variant = JavaScript<\/li>\n<\/ul>\n<p>This will create a new folder named todo-frontend in the src directory and then scaffold the React app into that folder. After the app has been scaffolded, <code>npm<\/code> will tell you to execute the following commands to initialize the app.<\/p>\n<ul>\n<li>cd todo-frontend<\/li>\n<li>npm install<\/li>\n<li>npm run dev<\/li>\n<\/ul>\n<p>These commands will install the dependencies and run the app to ensure that there are no issues. If you encounter and error, delete the todo-frontend folder and try again. You can\nuse <code>CTRL-C<\/code> to exit the app after you execute <code>npm run dev<\/code>. Now that we have a working front-end, let\u2019s integrate it with the AppHost. We will do that with the Aspire CLI.<\/p>\n<p>We will use the <a href=\"https:\/\/www.nuget.org\/packages\/Aspire.Cli\">Aspire CLI<\/a> to help us integrate the front-end with the AppHost. We will install the node integration package in the\nAppHost project.\nAspire integrations are NuGet packages that bootstrap config for you, and the Aspire CLI streamlines acquisition of them.\nExecute the commands below in the <code>src<\/code> directory.\nThis will add the package <code>Aspire.Hosting.NodeJs<\/code> into the AppHost project. It will enable some new extensions methods. Open up the AppHost.cs file in the TodojsAspire.AppHost.<\/p>\n<pre><code class=\"language-bash\">aspire add nodejs<\/code><\/pre>\n<p>Follow the prompts to add the package.<\/p>\n<p>We will add a Community Toolkit package to add Vite support. Execute the command below.<\/p>\n<pre><code class=\"language-bash\">aspire add ct-extensions<\/code><\/pre>\n<p>When prompted select <code>ct-extensions (CommunityToolkit.Aspire.Hosting.NodeJS.Extensions)<\/code>.<\/p>\n<p>project. Add the following to that file before <code>builder.Build().Run();<\/code>.<\/p>\n<pre><code class=\"language-csharp\">builder.AddViteApp(name: \"todo-frontend\", workingDirectory: \"..\/todo-frontend\")\r\n    .WithReference(apiService)\r\n    .WaitFor(apiService)\r\n    .WithNpmPackageInstallation();<\/code><\/pre>\n<p>This will add the front-end as an app in AppHost project and add integration with the dashboard. Now we need to configure the front-end to consume the port that the\nAppHost selects for the app. Open the <code>vite.config.js<\/code> file in the todo-frontend folder. Replace the existing content with the following.<\/p>\n<pre><code class=\"language-js\">import { defineConfig, loadEnv } from 'vite'\r\nimport react from '@vitejs\/plugin-react'\r\n\r\nexport default defineConfig(({ mode }) =&gt; {\r\n  const env = loadEnv(mode, process.cwd(), '');\r\n\r\n  return {\r\n    plugins: [react()],\r\n    server:{\r\n      port: parseInt(env.VITE_PORT),\r\n      proxy: {\r\n        \/\/ \"apiservice\" is the name of the API in AppHost.cs.\r\n        '\/api': {\r\n          target: process.env.services__apiservice__https__0 || process.env.services__apiservice__http__0,\r\n          changeOrigin: true,\r\n          secure: false,\r\n          rewrite: (path) =&gt; path.replace(\/^\\\/api\/, '')\r\n        }\r\n      }\r\n    },\r\n    build:{\r\n      outDir: 'dist',\r\n      rollupOptions: {\r\n        input: '.\/index.html'\r\n      }\r\n    }\r\n  }\r\n})<\/code><\/pre>\n<p>This will configure a proxy so that all commands are routed through the same origin, and it injects the URL for the ApiService. That\u2019s all the changes that are needed to integrate\nthe front-end with the AppHost. You can start the AppHost and you should see the front-end, along with the ApiService, in the dashboard.<\/p>\n<p><div class=\"alert alert-success\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Lightbulb\"><\/i><strong>Troubleshooting Vite.config.js load failure<\/strong><\/p>If you see an error that the vite.config.js file failed to load, run <code>npm install<\/code> in the todo-frontend folder, then press the play button next to the front-end in the Aspire Dashboard. You shouldn&#8217;t need to restart the AppHost.<\/div><\/p>\n<p>The dashboard should look like the following.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/07\/10-aspire-dashboard-with-react.png\" alt=\"Aspire dashboard showing all components including the todo-frontend React app, ApiService, and SQLite database\" \/><\/p>\n<p>If you click on the todo-frontend URL, you\u2019ll see the default Vite React template in the browser. Now we can start building our front-end. I\u2019ll walk you through all the steps\nneeded to get this app working.<\/p>\n<p>First let\u2019s add the components that we need for the todo app, and then we will update the files needed to use those components. In the todo-frontend\/src folder, add a\ncomponents folder. We will start with the component for a todo item, create an empty file in that folder named TodoItem.jsx. Paste in the contents below into that file.<\/p>\n<pre><code class=\"language-js\">\/**\r\n * TodoItem component represents a single task in the TODO list.\r\n * It displays the task text and provides buttons to delete the task,\r\n * move the task up, and move the task down in the list.\r\n *\r\n * @param {Object} props - The properties passed to the component.\r\n * @param {string} props.task - The text of the task.\r\n * @param {function} props.deleteTaskCallback - Callback function to delete the task.\r\n * @param {function} props.moveTaskUpCallback - Callback function to move the task up in the list.\r\n * @param {function} props.moveTaskDownCallback - Callback function to move the task down in the list.\r\n *\/\r\nfunction TodoItem({ task, deleteTaskCallback, moveTaskUpCallback, moveTaskDownCallback }) {\r\n  return (\r\n      &lt;li aria-label=\"task\"&gt;\r\n          &lt;span className=\"text\"&gt;{task}&lt;\/span&gt;\r\n          &lt;button\r\n              type=\"button\"\r\n              aria-label=\"Delete task\"\r\n              className=\"delete-button\"\r\n              onClick={() =&gt; deleteTaskCallback()}&gt;\r\n              \ud83d\uddd1\ufe0f\r\n          &lt;\/button&gt;\r\n          &lt;button\r\n              type=\"button\"\r\n              aria-label=\"Move task up\"\r\n              className=\"up-button\"\r\n              onClick={() =&gt; moveTaskUpCallback()}&gt;\r\n              \u21e7\r\n          &lt;\/button&gt;\r\n          &lt;button\r\n              type=\"button\"\r\n              aria-label=\"Move task down\"\r\n              className=\"down-button\"\r\n              onClick={() =&gt; moveTaskDownCallback()}&gt;\r\n              \u21e9\r\n          &lt;\/button&gt;\r\n      &lt;\/li&gt;\r\n  );\r\n}\r\n\r\nexport default TodoItem;<\/code><\/pre>\n<p>This is a basic component that will be used to display the todo item as well as elements for the actions; move up, move down and delete. We will use this component in the TodoList\ncomponent that we add next. We will wire up the buttons to actions in the list component. Add a new file named TodoList.jsx in the components folder and add the following content.<\/p>\n<pre><code class=\"language-js\">import { useState, useEffect } from 'react';\r\nimport '.\/TodoList.css';\r\nimport TodoItem from '.\/TodoItem';\r\n\r\n\/**\r\n * Todo component represents the main TODO list application.\r\n * It allows users to add new todos, delete todos, and move todos up or down in the list.\r\n * The component maintains the state of the todo list and the new todo input.\r\n *\/\r\nfunction TodoList() {\r\n    const [tasks, setTasks] = useState([]);\r\n    const [newTaskText, setNewTaskText] = useState('');\r\n    const [todos, setTodo] = useState([]);\r\n\r\n    const getTodo = async ()=&gt;{\r\n        fetch(\"\/api\/Todo\")\r\n        .then(response =&gt; response.json())\r\n        .then(json =&gt; setTodo(json))\r\n        .catch(error =&gt; console.error('Error fetching todos:', error));\r\n    }\r\n\r\n    useEffect(() =&gt; {\r\n        getTodo();\r\n    },[]);\r\n\r\n    function handleInputChange(event) {\r\n        setNewTaskText(event.target.value);\r\n    }\r\n\r\n    async function addTask(event) {\r\n        event.preventDefault();\r\n        if (newTaskText.trim()) {\r\n            \/\/ call the API to add the new task\r\n            const result = await fetch(\"\/api\/Todo\", {\r\n                method: \"POST\",\r\n                headers: {\r\n                    \"Content-Type\": \"application\/json\"\r\n                },\r\n                body: JSON.stringify({ title: newTaskText, isCompleted: false })\r\n            })\r\n            if(result.ok){\r\n                await getTodo();\r\n            }\r\n            \/\/ TODO: Add some error handling here, inform the user if there was a problem saving the TODO item.\r\n\r\n            setNewTaskText('');\r\n        }\r\n    }\r\n\r\n    async function deleteTask(id) {\r\n        console.log(`deleting todo ${id}`);\r\n        const result = await fetch(`\/api\/Todo\/${id}`, {\r\n            method: \"DELETE\"\r\n        });\r\n\r\n        if(result.ok){\r\n            await getTodo();\r\n        }\r\n        \/\/ TODO: Add some error handling here, inform the user if there was a problem saving the TODO item.\r\n    }\r\n\r\n    async function moveTaskUp(index) {\r\n        console.log(`moving todo ${index} up`);\r\n        const todo = todos[index];\r\n        const result = await fetch(`\/api\/Todo\/move-up\/${todo.id}`,{\r\n            method: \"POST\"\r\n        });\r\n\r\n        if(result.ok){\r\n            await getTodo();\r\n        }\r\n        else{\r\n            console.error('Error moving task up:', result.statusText);\r\n        }\r\n    }\r\n\r\n    async function moveTaskDown(index) {\r\n        const todo = todos[index];\r\n        const result = await fetch(`\/api\/Todo\/move-down\/${todo.id}`,{\r\n            method: \"POST\"\r\n        });\r\n\r\n        if(result.ok) {\r\n            await getTodo();\r\n        } else {\r\n            console.error('Error moving task down:', result.statusText);\r\n        }\r\n    }\r\n\r\n    return (\r\n    &lt;article\r\n        className=\"todo-list\"\r\n        aria-label=\"task list manager\"&gt;\r\n        &lt;header&gt;\r\n            &lt;h1&gt;TODO&lt;\/h1&gt;\r\n                &lt;form\r\n                    className=\"todo-input\"\r\n                    onSubmit={addTask}\r\n                    aria-controls=\"todo-list\"&gt;\r\n                &lt;input\r\n                    type=\"text\"\r\n                    required\r\n                    autoFocus\r\n                    placeholder=\"Enter a task\"\r\n                    value={newTaskText}\r\n                    aria-label=\"Task text\"\r\n                    onChange={handleInputChange} \/&gt;\r\n                &lt;button\r\n                    className=\"add-button\"\r\n                    aria-label=\"Add task\"&gt;\r\n                    Add\r\n                &lt;\/button&gt;\r\n            &lt;\/form&gt;\r\n        &lt;\/header&gt;\r\n        &lt;ol id=\"todo-list\" aria-live=\"polite\" aria-label=\"task list\"&gt;\r\n            {todos.map((task, index) =&gt;\r\n                &lt;TodoItem\r\n                    key={task.id}\r\n                    task={task.title}\r\n                    deleteTaskCallback={() =&gt; deleteTask(task.id)}\r\n                    moveTaskUpCallback={() =&gt; moveTaskUp(index)}\r\n                    moveTaskDownCallback={() =&gt; moveTaskDown(index)}\r\n                \/&gt;\r\n            )}\r\n        &lt;\/ol&gt;\r\n    &lt;\/article&gt;\r\n    );\r\n}\r\n\r\nexport default TodoList;<\/code><\/pre>\n<p>This component will display the list of todo items in our front-end. It fetches the todo items from the ApiService app, and all actions will be sent to that API for persistence.\nNotice that the fetch calls prefix the route with <code>\/api<\/code>, this comes from the configuration of the proxy in vite.config.js. The <code>moveTaskDown<\/code> and <code>moveTaskUp<\/code> functions call the related endpoint in the API project. Next add a new file named TodoList.css in the components folder with the following content.\nThe code from above already references this css file.<\/p>\n<pre><code class=\"language-css\">.todo-list {\r\n    background-color: #1e1e1e;\r\n    padding: 1.25rem;\r\n    border-radius: 0.5rem;\r\n    box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.3);\r\n    width: 100%;\r\n    max-width: 25rem;\r\n}\r\n\r\n.todo-list h1 {\r\n    text-align: center;\r\n    color: #e0e0e0;\r\n}\r\n\r\n.todo-input {\r\n    display: flex;\r\n    justify-content: space-between;\r\n    margin-bottom: 1.25rem;\r\n}\r\n\r\n.todo-input input {\r\n    flex: 1;\r\n    padding: 0.625rem;\r\n    border: 0.0625rem solid #333;\r\n    border-radius: 0.25rem;\r\n    margin-right: 0.625rem;\r\n    background-color: #2c2c2c;\r\n    color: #e0e0e0;\r\n}\r\n\r\n.todo-input .add-button {\r\n    padding: 0.625rem 1.25rem;\r\n    background-color: #007bff;\r\n    color: #fff;\r\n    border: none;\r\n    border-radius: 0.25rem;\r\n    cursor: pointer;\r\n}\r\n\r\n.todo-input .add-button:hover {\r\n    background-color: #0056b3;\r\n}\r\n\r\n.todo-list ol {\r\n    list-style-type: none;\r\n    padding: 0;\r\n}\r\n\r\n.todo-list li {\r\n    display: flex;\r\n    justify-content: space-between;\r\n    align-items: center;\r\n    padding: 0.625rem;\r\n    border-bottom: 0.0625rem solid #333;\r\n}\r\n\r\n.todo-list li:last-child {\r\n    border-bottom: none;\r\n}\r\n\r\n.todo-list .text {\r\n    flex: 1;\r\n}\r\n\r\n.todo-list li button {\r\n    background: none;\r\n    border: none;\r\n    cursor: pointer;\r\n    font-size: 1rem;\r\n    margin-left: 0.625rem;\r\n    color: #e0e0e0;\r\n}\r\n\r\n.todo-list li button:hover {\r\n    color: #007bff;\r\n}\r\n\r\n.todo-list li button.delete-button {\r\n    color: #ff4d4d;\r\n}\r\n\r\n.todo-list li button.up-button,\r\n.todo-list li button.down-button {\r\n    color: #4caf50;\r\n}<\/code><\/pre>\n<p>This file is straightforward CSS and doesn\u2019t need much explanation for front-end developers. Now that we have added the components, we need to update the app to work with\nthese components. Open up the main.jsx file in the root of the todo-frontend folder. In createRoot replace \u201croot\u201d with main. The code should look like the following.<\/p>\n<p>Update the contents of src\/main.jsx in todo-frontend to the code below.<\/p>\n<pre><code class=\"language-js\">import { StrictMode } from 'react'\r\nimport { createRoot } from 'react-dom\/client'\r\nimport '.\/index.css'\r\nimport App from '.\/App.jsx'\r\n\r\ncreateRoot(document.querySelector('main')).render(\r\n  &lt;StrictMode&gt;\r\n    &lt;App \/&gt;\r\n  &lt;\/StrictMode&gt;,\r\n)<\/code><\/pre>\n<p>Open App.jsx and replace the content with the following.<\/p>\n<pre><code class=\"language-js\">import TodoList from \".\/components\/TodoList\"\r\n\r\nfunction App() {\r\n    return (\r\n        &lt;TodoList \/&gt;\r\n    )\r\n}\r\n\r\nexport default App<\/code><\/pre>\n<p>Open index.css and replace the contents with the CSS below.<\/p>\n<pre><code class=\"language-css\">:root {\r\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\r\n  line-height: 1.5;\r\n  font-weight: 400;\r\n\r\n  color-scheme: light dark;\r\n  color: rgba(255, 255, 255, 0.87);\r\n  background-color: #242424;\r\n}\r\nbody {\r\n    font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\r\n    background-color: #121212;\r\n    color: #e0e0e0;\r\n    margin: 0;\r\n    padding: 0;\r\n    display: flex;\r\n    justify-content: center;\r\n    align-items: center;\r\n    height: 100vh;\r\n}<\/code><\/pre>\n<p>Finally, update the content of index.html to have the content below<\/p>\n<pre><code class=\"language-html\">&lt;!doctype html&gt;\r\n&lt;html lang=\"en\"&gt;\r\n  &lt;head&gt;\r\n    &lt;meta charset=\"UTF-8\" \/&gt;\r\n    &lt;link rel=\"icon\" type=\"image\/svg+xml\" href=\"\/checkmark-square.svg\" \/&gt;\r\n    &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" \/&gt;\r\n    &lt;title&gt;TODO app&lt;\/title&gt;\r\n    &lt;link href=\"https:\/\/fonts.googleapis.com\/css?family=Inter\" rel=\"stylesheet\"&gt;\r\n    &lt;script defer type=\"module\" src=\"\/src\/main.jsx\"&gt;&lt;\/script&gt;\r\n  &lt;\/head&gt;\r\n    &lt;body&gt;\r\n      &lt;main&gt;&lt;\/main&gt;\r\n    &lt;\/body&gt;\r\n&lt;\/html&gt;<\/code><\/pre>\n<p>Now we have updated the app and it should be working. Start the AppHost project and then click on the URL for the front-end in the dashboard. Below is a video of the app running.<\/p>\n<p><div style=\"width: 640px;\" class=\"wp-video\"><video class=\"wp-video-shortcode\" id=\"video-57395-4\" width=\"640\" height=\"360\" preload=\"metadata\" controls=\"controls\"><source type=\"video\/mp4\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/06\/11-todojsaspire-app-running.mp4?_=4\" \/><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/06\/11-todojsaspire-app-running.mp4\">https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/06\/11-todojsaspire-app-running.mp4<\/a><\/video><\/div><\/p>\n<p>Our app is now working. You can use the dashboard to view telemetry flowing automatically between the database, .NET Web API backend, and React front-end. I didn&#8217;t go into much detail on the React code here. I wrote a similar blog post for Visual Studio users which covers the React parts in more details <a href=\"https:\/\/devblogs.microsoft.com\/visualstudio\/creating-a-react-todo-app-in-visual-studio-2022\/\">Creating a React TODO app in Visual Studio 2022<\/a>.\nI\u2019ll now move on to wrap up this post.<\/p>\n<h2>Looking forward<\/h2>\n<p>Now that we have the app running locally, the next step would be to deploy this to production. You can deploy this to any web host that supports ASP.NET Core.\nWe won&#8217;t go through that here, but we may revisit that in a future post.<\/p>\n<h2>Recap<\/h2>\n<p>In this post, we built a new Aspire app with an ASP.NET Core Web API and connected it to a React front end using JavaScript. We worked entirely from the command line and C# Dev Kit, leveraging the new Aspire CLI and <code>dotnet scaffold<\/code> to add database support with SQLite.<\/p>\n<h2>Feedback<\/h2>\n<p>For feedback on Aspire please file an issue in this repo <a href=\"https:\/\/github.com\/dotnet\/aspire\">dotnet\/aspire<\/a>. For feedback related to dotnet scaffold, the correct repo for\nissues is <a href=\"https:\/\/github.com\/dotnet\/scaffolding\">dotnet\/Scaffolding<\/a>. Feedback related to C# Dev Kit can go to\n<a href=\"https:\/\/github.com\/microsoft\/vscode-dotnettools\">microsoft\/vscode-dotnettools<\/a>. You can comment below as well. If you enjoy this type of content, please leave a comment below\nexpressing your support. This will enable us to produce more posts of a similar nature.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Discover how to build a full-stack application with React and Aspire, integrating a React front-end with an ASP.NET Core Web API and persisting data to a database.<\/p>\n","protected":false},"author":357,"featured_media":57414,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7783],"tags":[4,32,7768,46,148],"class_list":["post-57395","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-aspire","tag-net","tag-asp-net-core","tag-aspire","tag-c","tag-visual-studio-code"],"acf":[],"blog_post_summary":"<p>Discover how to build a full-stack application with React and Aspire, integrating a React front-end with an ASP.NET Core Web API and persisting data to a database.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/57395","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/357"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=57395"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/57395\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/57414"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=57395"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=57395"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=57395"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}