{"id":2311,"date":"2025-07-30T18:16:19","date_gmt":"2025-07-30T18:16:19","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2025\/07\/30\/building-a-full-stack-app-with-react-and-aspire-a-step-by-step-guide\/"},"modified":"2025-07-30T18:16:19","modified_gmt":"2025-07-30T18:16:19","slug":"building-a-full-stack-app-with-react-and-aspire-a-step-by-step-guide","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2025\/07\/30\/building-a-full-stack-app-with-react-and-aspire-a-step-by-step-guide\/","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 a new we will build a TODO app from start to finish, using Aspire and React. We will use Aspire and React to create a full stack TODO application.<br \/>\nWe will do this using the CLI and C# Dev Kit. The todo items will be stored in a SQLite database.<br \/>\nThe React front-end will use a Web API to handle all the interactions with the data.<br \/>\nI\u2019m 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.<br \/>\nThe resulting app can be published to any web host which supports ASP.NET Core \u2013 including Linux containers.<br \/>\nFirst let\u2019s start with the prerequisites to ensure you have all the components needed to follow along this tutorial.<\/p>\n\n<div class=\"alert alert-success\">\n<p class=\"alert-divider\"><strong>Source Code<\/strong><\/p>\n<p>All the code from this post can be found at <a href=\"https:\/\/github.com\/sayedihashimi\/todojsaspire\">sayedihashimi\/todojsaspire<\/a>.<\/p><\/div>\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<p><a href=\"https:\/\/dotnet.microsoft.com\/download\/dotnet\/9.0\">.NET 9.0<\/a><br \/>\n<a href=\"https:\/\/nodejs.org\/en\/download\">nodejs<\/a><br \/>\nVS Code with <a href=\"https:\/\/marketplace.visualstudio.com\/items?itemName=ms-dotnettools.csdevkit\">C# Dev Kit<\/a><br \/>\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/fundamentals\/setup-tooling?tabs=windows&amp;pivots=vscode#container-runtime\">Container runtime<\/a><\/p>\n<h2>Install Aspire<\/h2>\n<p>For detailed instructions on getting Aspire, and its dependencies, installed visit<br \/>\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.<br \/>\nAfter installing .NET 9 and the other dependencies we will install the project templates using dotnet new.<\/p>\n\n<div class=\"alert alert-info\">\n<p class=\"alert-divider\"><strong>Workload Migration<\/strong><\/p>\n<p>As of version 9, Aspire no longer requires a separate workload installation. Use dotnet workload list to check installed workloads and dotnet workload uninstall to remove the Aspire workload.<\/p><\/div>\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 dotnet new templates.<\/p>\n<p>On Windows:<\/p>\n<p>iex &#8220;&amp; { $(irm https:\/\/aspire.dev\/install.ps1) }&#8221;<\/p>\n<p>On Linux, or macOS:<\/p>\n<p>curl -sSL https:\/\/aspire.dev\/install.sh | bash -s<\/p>\n<p>After installing this tool, you can run it by executing aspire on the command line. You can explore the usage of this tool with aspire -\u2013help. 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.<br \/>\nOpen an empty folder in VS Code and add a new directory named src for the source files.<\/p>\n<p>Let\u2019s create the Aspire app to start with. In VS Code open the command palette<br \/>\nCTRL\/CMD-SHIFT-P and type in New Project. See the following image.<\/p>\n\n<p>Select the Aspire Starter App template and hit enter.<\/p>\n\n<p>When prompted for the project name use \u201cTodojsAspire\u201d and select \u201csrc\u201d as the destination folder to follow along.<br \/>\nI will walk you through using New Project to create the Aspire app in the video below. Alternatively, you can use dotnet new aspire-starter or aspire new aspire-starter<br \/>\nin a terminal for the same result.<\/p>\n\n<div class=\"wp-video\"><!--[if lt IE 9]&gt;document.createElement('video');&lt;![endif]--><br \/>\n<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><\/div>\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 .gitattributes, .gitignore and LICENSE.<\/p>\n\n<p>Now would be a good time to execute a build to ensure that there are no build issues.<br \/>\nOpen the command palette with CTRL\/CMD-SHIFT-P and select \u201c.NET: Build\u201d. 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,<br \/>\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<br \/>\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\n<p>After deleting the project we need to remove any references to it. The things that need to be removed include.<\/p>\n<p>Project reference in TodojsAspire.AppHost<br \/>\nUpdate AppHost in TodojsAspire.AppHost<\/p>\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<br \/>\nsame project.<\/p>\n<p>builder.AddProject&lt;Projects.TodojsAspire_Web&gt;(&#8220;webfrontend&#8221;)<br \/>\n    .WithExternalHttpEndpoints()<br \/>\n    .WithHttpHealthCheck(&#8220;\/health&#8221;)<br \/>\n    .WithReference(apiService)<br \/>\n    .WaitFor(apiService);<\/p>\n<p>Soon we will replace these lines with what is needed to integrate the React app.<br \/>\nYou should also delete the TodojsAspire.Web folder from the src directory.<br \/>\nAfter making those changes, you should do a build to ensure that nothing was missed. To start a<br \/>\nbuild, open the command palette and select Task: Run Build Task and then select dotnet: build.<br \/>\nNow that we have cleaned up the solution, we will move on to start updating the API project to expose endpoints to manage<br \/>\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 dotnet scaffold to generate the initial API endpoints.<br \/>\nAdd the Todo class (Todo.cs) below to the TodojsAspire.ApiService project.<\/p>\n<p>using System.ComponentModel.DataAnnotations;<br \/>\nnamespace TodojsAspire.ApiService;<\/p>\n<p>public class Todo<br \/>\n{<br \/>\n    public int Id { get; set; }<br \/>\n    [Required]<br \/>\n    public string Title { get; set; } = default!;<br \/>\n    public bool IsComplete { get; set; } = false;<br \/>\n    \/\/ The position of the todo in the list, used for ordering.<br \/>\n    \/\/ When updating this, make sure to not duplicate values.<br \/>\n    \/\/ To move an item up\/down, swap the values of the position<br \/>\n    [Required]<br \/>\n    public int Position { get; set; } = 0;<br \/>\n}<\/p>\n<p>Now that we have added the model class, we will scaffold the API endpoints with dotnet scaffold.<\/p>\n<p>We can use dotnet scaffold to generate API endpoints for the Todo model. To install this tool, execute the following command.<\/p>\n<p>dotnet tool install &#8211;global Microsoft.dotnet-scaffold<\/p>\n<p>When using dotnet scaffold it\u2019s easiest to cd into the project directory and then execute it from there. This tool is interactive by default, to get started execute dotnet scaffold. Make the following selections.<\/p>\n<p>Category = API<br \/>\nCommand = Minimal API<br \/>\nProject = TodojsAspire.ApiService<br \/>\nModel = Todo<br \/>\nEndpoints file name = TodoEndpoints<br \/>\nOpen API Enabled = No<br \/>\nData context class = TodoDbContext<br \/>\nDatabase provider = sqlite-efcore<br \/>\nInclude prerelease = No<\/p>\n<p>You can see the entire interaction in the following animation.<\/p>\n\n<div class=\"wp-video\"><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><\/div>\n<p>The following changes were made to the TodojsAspire.ApiService project.<\/p>\n<p>TodoEndpoints.cs file was created with the Minimal API endpoints.<br \/>\nProgram.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.<br \/>\nThe project file was modified to add needed NuGet packages.<br \/>\nappsettings.json was modified to add the connection to the local db file.<\/p>\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<p>Microsoft.EntityFrameworkCore<br \/>\nMicrosoft.EntityFrameworkCore.Design<br \/>\nMicrosoft.EntityFrameworkCore.Sqlite<br \/>\nMicrosoft.EntityFrameworkCore.Tools<br \/>\nSystem.ComponentModel.Annotations<\/p>\n<p>You can install packages using dotnet add package [PACKAGE NAME].<\/p>\n<p>Open the new file TodoEndpoints.cs so that we can take a look.<br \/>\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 \/api\/ with \/.<br \/>\nThe resulting class, TodoEndpoints.cs, is below.<\/p>\n<p>using Microsoft.AspNetCore.Http.HttpResults;<br \/>\nusing Microsoft.EntityFrameworkCore;<br \/>\nusing TodojsAspire.ApiService;<\/p>\n<p>public static class TodoEndpoints<br \/>\n{<br \/>\n    public static void MapTodoEndpoints(this IEndpointRouteBuilder routes)<br \/>\n    {<br \/>\n        var group = routes.MapGroup(&#8220;\/Todo&#8221;);<\/p>\n<p>        group.MapGet(&#8220;\/&#8221;, async (TodoDbContext db) =&gt;<br \/>\n        {<br \/>\n            return await db.Todo.ToListAsync();<br \/>\n        })<br \/>\n        .WithName(&#8220;GetAllTodos&#8221;);<\/p>\n<p>        group.MapGet(&#8220;\/{id}&#8221;, async Task&lt;Results&lt;Ok&lt;Todo&gt;, NotFound&gt;&gt; (int id, TodoDbContext db) =&gt;<br \/>\n        {<br \/>\n            return await db.Todo.AsNoTracking()<br \/>\n                .FirstOrDefaultAsync(model =&gt; model.Id == id)<br \/>\n                is Todo model<br \/>\n                    ? TypedResults.Ok(model)<br \/>\n                    : TypedResults.NotFound();<br \/>\n        })<br \/>\n        .WithName(&#8220;GetTodoById&#8221;);<\/p>\n<p>        group.MapPut(&#8220;\/{id}&#8221;, async Task&lt;Results&lt;Ok, NotFound&gt;&gt; (int id, Todo todo, TodoDbContext db) =&gt;<br \/>\n        {<br \/>\n            var affected = await db.Todo<br \/>\n                .Where(model =&gt; model.Id == id)<br \/>\n                .ExecuteUpdateAsync(setters =&gt; setters<br \/>\n                .SetProperty(m =&gt; m.Title, todo.Title)<br \/>\n                .SetProperty(m =&gt; m.IsComplete, todo.IsComplete)<br \/>\n                .SetProperty(m =&gt; m.Position, todo.Position)<br \/>\n        );<\/p>\n<p>            return affected == 1 ? TypedResults.Ok() : TypedResults.NotFound();<br \/>\n        })<br \/>\n        .WithName(&#8220;UpdateTodo&#8221;);<\/p>\n<p>        group.MapPost(&#8220;\/&#8221;, async (Todo todo, TodoDbContext db) =&gt;<br \/>\n        {<br \/>\n            db.Todo.Add(todo);<br \/>\n            await db.SaveChangesAsync();<br \/>\n            return TypedResults.Created($&#8221;\/Todo\/{todo.Id}&#8221;,todo);<br \/>\n        })<br \/>\n        .WithName(&#8220;CreateTodo&#8221;);<\/p>\n<p>        group.MapDelete(&#8220;\/{id}&#8221;, async Task&lt;Results&lt;Ok, NotFound&gt;&gt; (int id, TodoDbContext db) =&gt;<br \/>\n        {<br \/>\n            var affected = await db.Todo<br \/>\n                .Where(model =&gt; model.Id == id)<br \/>\n                .ExecuteDeleteAsync();<\/p>\n<p>            return affected == 1 ? TypedResults.Ok() : TypedResults.NotFound();<br \/>\n        })<br \/>\n        .WithName(&#8220;DeleteTodo&#8221;);<br \/>\n    }<br \/>\n}<\/p>\n<p>This file contains the CRUD methods which are needed to support reading\/writing the content from the database.<br \/>\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<br \/>\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; MoveTaskUp and MoveTaskDown.<br \/>\nThe code for these endpoints are below, add it below the last endpoint in the TodoEndpoints class.<\/p>\n<p>\/\/ Endpoint to move a task up in the list<br \/>\ngroup.MapPost(&#8220;\/move-up\/{id:int}&#8221;, async Task&lt;Results&lt;Ok, NotFound&gt;&gt; (int id, TodoDbContext db) =&gt;<br \/>\n{<br \/>\n    var todo = await db.Todo.FirstOrDefaultAsync(t =&gt; t.Id == id);<br \/>\n    if (todo is null)<br \/>\n    { return TypedResults.NotFound(); }<\/p>\n<p>    \/\/ Find the todo with the largest position less than the current todo<br \/>\n    var prevTodo = await db.Todo<br \/>\n        .Where(t =&gt; t.Position &lt; todo.Position)<br \/>\n        .OrderByDescending(t =&gt; t.Position)<br \/>\n        .FirstOrDefaultAsync();<\/p>\n<p>    if (prevTodo is null)<br \/>\n    { return TypedResults.Ok(); }<\/p>\n<p>    \/\/ Swap positions<br \/>\n    (todo.Position, prevTodo.Position) = (prevTodo.Position, todo.Position);<br \/>\n    await db.SaveChangesAsync();<br \/>\n    return TypedResults.Ok();<br \/>\n})<br \/>\n.WithName(&#8220;MoveTaskUp&#8221;);<\/p>\n<p>\/\/ Endpoint to move a task down in the list<br \/>\ngroup.MapPost(&#8220;\/move-down\/{id:int}&#8221;, async Task&lt;Results&lt;Ok, NotFound&gt;&gt; (int id, TodoDbContext db) =&gt;<br \/>\n{<br \/>\n    var todo = await db.Todo.FirstOrDefaultAsync(t =&gt; t.Id == id);<br \/>\n    if (todo is null)<br \/>\n    { return TypedResults.NotFound(); }<\/p>\n<p>    \/\/ Find the todo with the smallest position greater than the current todo<br \/>\n    var nextTodo = await db.Todo<br \/>\n        .Where(t =&gt; t.Position &gt; todo.Position)<br \/>\n        .OrderBy(t =&gt; t.Position)<br \/>\n        .FirstOrDefaultAsync();<\/p>\n<p>    if (nextTodo is null)<br \/>\n    { return TypedResults.Ok(); } \/\/ Already at the bottom or no next todo<\/p>\n<p>    \/\/ Swap positions values<br \/>\n    (todo.Position, nextTodo.Position) = (nextTodo.Position, todo.Position);<br \/>\n    await db.SaveChangesAsync();<br \/>\n    return TypedResults.Ok();<br \/>\n})<br \/>\n.WithName(&#8220;MoveTaskDown&#8221;);<\/p>\n<p>MoveTaskUp will find the task with a next lower position, and then swaps the position values. This line of code<br \/>\n(todo.Position, prevTodo.Position) = (prevTodo.Position, todo.Position); 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,<br \/>\ncd into the TodojsAspire.ApiService project directory (src\/TodojsAspire.ApiService). Then execute the following command.<\/p>\n<p>dotnet ef migrations add TodoEndpointsInitialCreate<\/p>\n<p>The migrations command will generate a new migration named TodoEndpointsInitialCreate and add it to the project. At this time you would typically also run dotnet ef database update but that isn\u2019t needed in this case.<br \/>\nWe will configure the project to run migrations when it is started by the AppHost.<br \/>\nLet\u2019s 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 \u201csrc\u201d folder to install SQLite support in the AppHost.<\/p>\n<p>aspire add sqlite<\/p>\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<p>var builder = DistributedApplication.CreateBuilder(args);<\/p>\n<p>var db = builder.AddSqlite(&#8220;db&#8221;)<br \/>\n    .WithSqliteWeb();<\/p>\n<p>var apiService = builder.AddProject&lt;Projects.TodojsAspire_ApiService&gt;(&#8220;apiservice&#8221;)<br \/>\n    .WithReference(db)<br \/>\n    .WithHttpHealthCheck(&#8220;\/health&#8221;);<\/p>\n<p>builder.Build().Run();<\/p>\n<p>In AppHost.cs we have added a SQLite database and registered the API service. We called WithReference(db) on the API so that it gets the connection<br \/>\nstring to the database.<\/p>\n<p>To configure the ApiService we will need to add the package CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite and update the connection<br \/>\nto the database. In a terminal first cd into the ApiService project and execute the command below.<\/p>\n<p>dotnet add package CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite<\/p>\n<p>Modify the Program.cs in the Api project to have the following contents.<\/p>\n<p>using Microsoft.EntityFrameworkCore;<\/p>\n<p>var builder = WebApplication.CreateBuilder(args);<\/p>\n<p>builder.AddSqliteDbContext&lt;TodoDbContext&gt;(&#8220;db&#8221;);<\/p>\n<p>\/\/ Add service defaults &amp; Aspire client integrations.<br \/>\nbuilder.AddServiceDefaults();<\/p>\n<p>\/\/ Add services to the container.<br \/>\nbuilder.Services.AddProblemDetails();<\/p>\n<p>\/\/ Learn more about configuring OpenAPI at https:\/\/aka.ms\/aspnet\/openapi<br \/>\nbuilder.Services.AddOpenApi();<\/p>\n<p>var app = builder.Build();<\/p>\n<p>\/\/ Configure the HTTP request pipeline.<br \/>\napp.UseExceptionHandler();<\/p>\n<p>if (app.Environment.IsDevelopment())<br \/>\n{<br \/>\n    app.MapOpenApi();<br \/>\n}<\/p>\n<p>app.MapDefaultEndpoints();<\/p>\n<p>app.MapTodoEndpoints();<\/p>\n<p>using var scope = app.Services.CreateScope();<br \/>\nvar dbContext = scope.ServiceProvider.GetRequiredService&lt;TodoDbContext&gt;();<br \/>\nawait dbContext.Database.MigrateAsync();<\/p>\n<p>app.Run();<\/p>\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<br \/>\nappsettings.json file from the API project, it\u2019s now being injected with builder.AddSqliteDbContext&lt;TodoDbContext&gt;(&#8220;db&#8221;). You should remove the<br \/>\nconnection string from the appsettings.json file now.<br \/>\nAt the bottom of Program.cs we have added await dbContext.Database.MigrateAsync() to ensure that the database is up-to-date when<br \/>\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\u2019s working as expected<\/h2>\n<p>Now that we have all the<br \/>\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<br \/>\nthat you can pick from, including <a href=\"https:\/\/marketplace.visualstudio.com\/items?itemName=humao.rest-client\">REST Client<\/a> and<br \/>\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<br \/>\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<br \/>\nin the search box, then click Install. See the next image.<\/p>\n\n<p>In the TodojsAspire.ApiService project open the file named TodojsAspire.ApiService.http. If your project doesn\u2019t have a file with that name, create a new one.<br \/>\nThe name of the HTTP file doesn\u2019t matter; you can name it whatever you like.<br \/>\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;<br \/>\nyou can use Start Debugging (F5) or Start without Debugging (CTRL-F5). In this case we don\u2019t need to debug so we can use the keyboard shortcut CTRL-F5 to Start without Debugging and<br \/>\nchoose App Host [default configuration].<br \/>\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<br \/>\nthe launch configuration, choose the AppHost project. This will start the Aspire Dashboard and it will automatically startup the ApiService as well.<\/p>\n\n<div class=\"wp-video\"><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><\/div>\n<p>For detailed info on the dashboard, see this article<br \/>\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/fundamentals\/dashboard\/overview?tabs=bash\">Aspire dashboard overview \u2013 Aspire | Microsoft Learn<\/a>. We will go over the<br \/>\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<p>Real-time tracking of logs, traces, and environment configurations.<br \/>\nUser interface to <a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/fundamentals\/dashboard\/explore#resource-actions\">stop, start, and restart resources<\/a>.<br \/>\nCollects 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.<br \/>\nEnhanced debugging with <a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/fundamentals\/dashboard\/copilot\">GitHub Copilot<\/a>, your AI-powered assistant built into the dashboard.<\/p>\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<br \/>\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\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 db-sqliteweb to open a web interface to interact with the database, but that isn\u2019t 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<br \/>\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<br \/>\n<a href=\"https:\/\/marketplace.visualstudio.com\/items?itemName=humao.rest-client\">REST Client documentation<\/a> or<br \/>\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><br \/>\n(<em>note: some of the features described aren\u2019t supported outside of Visual Studio 2022<\/em>).<\/p>\n<p>@todoapibaseurl = https:\/\/localhost:7473<\/p>\n<p>GET {{todoapibaseurl}}\/Todo\/<\/p>\n<p>###<\/p>\n<p># Create a new todo<br \/>\nPOST {{todoapibaseurl}}\/Todo\/<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#8220;title&#8221;: &#8220;Sample Todo2&#8221;,<br \/>\n  &#8220;isComplete&#8221;: false,<br \/>\n  &#8220;position&#8221;: 1<br \/>\n}<\/p>\n<p>###<br \/>\nPOST {{todoapibaseurl}}\/Todo\/<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#8220;title&#8221;: &#8220;Sample Todo2&#8221;,<br \/>\n  &#8220;isComplete&#8221;: false,<br \/>\n  &#8220;position&#8221;: 2<br \/>\n}<br \/>\n###<br \/>\nPOST {{todoapibaseurl}}\/Todo\/<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#8220;title&#8221;: &#8220;Sample Todo3&#8221;,<br \/>\n  &#8220;isComplete&#8221;: false,<br \/>\n  &#8220;position&#8221;: 3<br \/>\n}<\/p>\n<p>###<br \/>\nPUT {{todoapibaseurl}}\/Todo\/1<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#8220;id&#8221;: 1,<br \/>\n  &#8220;title&#8221;: &#8220;Updated Todo&#8221;,<br \/>\n  &#8220;isComplete&#8221;: true,<br \/>\n  &#8220;position&#8221;: 20<br \/>\n}<\/p>\n<p>###<\/p>\n<p>POST {{todoapibaseurl}}\/Todo\/<br \/>\nContent-Type: application\/json<\/p>\n<p>{<br \/>\n  &#8220;title&#8221;: &#8220;Sample Todo no position&#8221;,<br \/>\n  &#8220;isComplete&#8221;: false<br \/>\n}<br \/>\n###<\/p>\n<p># Delete a todo<br \/>\nDELETE {{todoapibaseurl}}\/Todo\/1<\/p>\n<p>###<\/p>\n<p>POST {{todoapibaseurl}}\/Todo\/move-up\/3<br \/>\n###<\/p>\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 Send Request above the URL line.<br \/>\nYou can also use Rest Client: Send Request in the command palette.<br \/>\nTry out the different requests to make sure things are working correctly.<br \/>\nRemember that the database will be wiped out when the app is restarted, so you<br \/>\ndon\u2019t need to worry about adding this data. When working with this file I noticed<br \/>\ntwo issues what should be addressed.<\/p>\n<p>When Todo items are returned, they are not sorted by Position.<br \/>\nWhen a Todo item is POSTed without a position, the value for position will be assigned to 0.<\/p>\n<p>To fix the first issue, specifically group.MapGet(&#8220;\/&#8221;,, update the get endpoint to have the following code.<\/p>\n<p>group.MapGet(&#8220;\/&#8221;, async (TodoDbContext db) =&gt;<br \/>\n{<br \/>\n    return await db.Todo.OrderBy(t =&gt; t.Position).ToListAsync();<br \/>\n})<br \/>\n.WithName(&#8220;GetAllTodos&#8221;);<\/p>\n<p>To fix the issue regarding the missing position value, update the POST method to have the following code.<\/p>\n<p>group.MapPost(&#8220;\/&#8221;, async (Todo todo, TodoDbContext db) =&gt;<br \/>\n{<br \/>\n    if (todo.Position &lt;= 0)<br \/>\n    {<br \/>\n        \/\/ If position is not set, assign it to the next available position<br \/>\n        todo.Position = await db.Todo.AnyAsync()<br \/>\n            ? await db.Todo.MaxAsync(t =&gt; t.Position) + 1<br \/>\n            : 1; \/\/ Start at position 1 if no todos exist<br \/>\n    }<br \/>\n    db.Todo.Add(todo);<br \/>\n    await db.SaveChangesAsync();<br \/>\n    return TypedResults.Created($&#8221;\/Todo\/{todo.Id}&#8221;, todo);<br \/>\n})<br \/>\n.WithName(&#8220;CreateTodo&#8221;);<\/p>\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<br \/>\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 npm 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.<br \/>\nWe will use <a href=\"https:\/\/vite.dev\/\">vite<\/a> as the front-end build tool.<\/p>\n<p>Open a terminal, cd into the src directory and then execute the command below.<\/p>\n<p>npm create vite@latest todo-frontend &#8212; &#8211;template react<\/p>\n<p>When prompted specify the following values.<\/p>\n<p>Framework = React<br \/>\nVariant = JavaScript<\/p>\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, npm will tell you to execute the following commands to initialize the app.<\/p>\n<p>cd todo-frontend<br \/>\nnpm install<br \/>\nnpm run dev<\/p>\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<br \/>\nuse CTRL-C to exit the app after you execute npm run dev. 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<br \/>\nAppHost project.<br \/>\nAspire integrations are NuGet packages that bootstrap config for you, and the Aspire CLI streamlines acquisition of them.<br \/>\nExecute the commands below in the src directory.<br \/>\nThis will add the package Aspire.Hosting.NodeJs into the AppHost project. It will enable some new extensions methods. Open up the AppHost.cs file in the TodojsAspire.AppHost.<\/p>\n<p>aspire add nodejs<\/p>\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<p>aspire add ct-extensions<\/p>\n<p>When prompted select ct-extensions (CommunityToolkit.Aspire.Hosting.NodeJS.Extensions).<\/p>\n<p>project. Add the following to that file before builder.Build().Run();.<\/p>\n<p>builder.AddViteApp(name: &#8220;todo-frontend&#8221;, workingDirectory: &#8220;..\/todo-frontend&#8221;)<br \/>\n    .WithReference(apiService)<br \/>\n    .WaitFor(apiService)<br \/>\n    .WithNpmPackageInstallation();<\/p>\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<br \/>\nAppHost selects for the app. Open the vite.config.js file in the todo-frontend folder. Replace the existing content with the following.<\/p>\n<p>import { defineConfig, loadEnv } from &#8216;vite&#8217;<br \/>\nimport react from &#8216;@vitejs\/plugin-react&#8217;<\/p>\n<p>export default defineConfig(({ mode }) =&gt; {<br \/>\n  const env = loadEnv(mode, process.cwd(), &#8221;);<\/p>\n<p>  return {<br \/>\n    plugins: [react()],<br \/>\n    server:{<br \/>\n      port: parseInt(env.VITE_PORT),<br \/>\n      proxy: {<br \/>\n        \/\/ &#8220;apiservice&#8221; is the name of the API in AppHost.cs.<br \/>\n        &#8216;\/api&#8217;: {<br \/>\n          target: process.env.services__apiservice__https__0 || process.env.services__apiservice__http__0,<br \/>\n          changeOrigin: true,<br \/>\n          secure: false,<br \/>\n          rewrite: (path) =&gt; path.replace(\/^\/api\/, &#8221;)<br \/>\n        }<br \/>\n      }<br \/>\n    },<br \/>\n    build:{<br \/>\n      outDir: &#8216;dist&#8217;,<br \/>\n      rollupOptions: {<br \/>\n        input: &#8216;.\/index.html&#8217;<br \/>\n      }<br \/>\n    }<br \/>\n  }<br \/>\n})<\/p>\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<br \/>\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\n<div class=\"alert alert-success\">\n<p class=\"alert-divider\"><strong>Troubleshooting Vite.config.js load failure<\/strong><\/p>\n<p>If you see an error that the vite.config.js file failed to load, run npm install in the todo-frontend folder, then press the play button next to the front-end in the Aspire Dashboard. You shouldn\u2019t need to restart the AppHost.<\/p><\/div>\n<p>The dashboard should look like the following.<\/p>\n\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<br \/>\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<br \/>\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<p>\/**<br \/>\n * TodoItem component represents a single task in the TODO list.<br \/>\n * It displays the task text and provides buttons to delete the task,<br \/>\n * move the task up, and move the task down in the list.<br \/>\n *<br \/>\n * @param {Object} props &#8211; The properties passed to the component.<br \/>\n * @param {string} props.task &#8211; The text of the task.<br \/>\n * @param {function} props.deleteTaskCallback &#8211; Callback function to delete the task.<br \/>\n * @param {function} props.moveTaskUpCallback &#8211; Callback function to move the task up in the list.<br \/>\n * @param {function} props.moveTaskDownCallback &#8211; Callback function to move the task down in the list.<br \/>\n *\/<br \/>\nfunction TodoItem({ task, deleteTaskCallback, moveTaskUpCallback, moveTaskDownCallback }) {<br \/>\n  return (<br \/>\n      &lt;li aria-label=&#8221;task&#8221;&gt;<br \/>\n          &lt;span className=&#8221;text&#8221;&gt;{task}&lt;\/span&gt;<br \/>\n          &lt;button<br \/>\n              type=&#8221;button&#8221;<br \/>\n              aria-label=&#8221;Delete task&#8221;<br \/>\n              className=&#8221;delete-button&#8221;<br \/>\n              onClick={() =&gt; deleteTaskCallback()}&gt;<br \/>\n              \ud83d\uddd1<br \/>\n          &lt;\/button&gt;<br \/>\n          &lt;button<br \/>\n              type=&#8221;button&#8221;<br \/>\n              aria-label=&#8221;Move task up&#8221;<br \/>\n              className=&#8221;up-button&#8221;<br \/>\n              onClick={() =&gt; moveTaskUpCallback()}&gt;<br \/>\n              \u21e7<br \/>\n          &lt;\/button&gt;<br \/>\n          &lt;button<br \/>\n              type=&#8221;button&#8221;<br \/>\n              aria-label=&#8221;Move task down&#8221;<br \/>\n              className=&#8221;down-button&#8221;<br \/>\n              onClick={() =&gt; moveTaskDownCallback()}&gt;<br \/>\n              \u21e9<br \/>\n          &lt;\/button&gt;<br \/>\n      &lt;\/li&gt;<br \/>\n  );<br \/>\n}<\/p>\n<p>export default TodoItem;<\/p>\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<br \/>\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<p>import { useState, useEffect } from &#8216;react&#8217;;<br \/>\nimport &#8216;.\/TodoList.css&#8217;;<br \/>\nimport TodoItem from &#8216;.\/TodoItem&#8217;;<\/p>\n<p>\/**<br \/>\n * Todo component represents the main TODO list application.<br \/>\n * It allows users to add new todos, delete todos, and move todos up or down in the list.<br \/>\n * The component maintains the state of the todo list and the new todo input.<br \/>\n *\/<br \/>\nfunction TodoList() {<br \/>\n    const [tasks, setTasks] = useState([]);<br \/>\n    const [newTaskText, setNewTaskText] = useState(&#8221;);<br \/>\n    const [todos, setTodo] = useState([]);<\/p>\n<p>    const getTodo = async ()=&gt;{<br \/>\n        fetch(&#8220;\/api\/Todo&#8221;)<br \/>\n        .then(response =&gt; response.json())<br \/>\n        .then(json =&gt; setTodo(json))<br \/>\n        .catch(error =&gt; console.error(&#8216;Error fetching todos:&#8217;, error));<br \/>\n    }<\/p>\n<p>    useEffect(() =&gt; {<br \/>\n        getTodo();<br \/>\n    },[]);<\/p>\n<p>    function handleInputChange(event) {<br \/>\n        setNewTaskText(event.target.value);<br \/>\n    }<\/p>\n<p>    async function addTask(event) {<br \/>\n        event.preventDefault();<br \/>\n        if (newTaskText.trim()) {<br \/>\n            \/\/ call the API to add the new task<br \/>\n            const result = await fetch(&#8220;\/api\/Todo&#8221;, {<br \/>\n                method: &#8220;POST&#8221;,<br \/>\n                headers: {<br \/>\n                    &#8220;Content-Type&#8221;: &#8220;application\/json&#8221;<br \/>\n                },<br \/>\n                body: JSON.stringify({ title: newTaskText, isCompleted: false })<br \/>\n            })<br \/>\n            if(result.ok){<br \/>\n                await getTodo();<br \/>\n            }<br \/>\n            \/\/ TODO: Add some error handling here, inform the user if there was a problem saving the TODO item.<\/p>\n<p>            setNewTaskText(&#8221;);<br \/>\n        }<br \/>\n    }<\/p>\n<p>    async function deleteTask(id) {<br \/>\n        console.log(`deleting todo ${id}`);<br \/>\n        const result = await fetch(`\/api\/Todo\/${id}`, {<br \/>\n            method: &#8220;DELETE&#8221;<br \/>\n        });<\/p>\n<p>        if(result.ok){<br \/>\n            await getTodo();<br \/>\n        }<br \/>\n        \/\/ TODO: Add some error handling here, inform the user if there was a problem saving the TODO item.<br \/>\n    }<\/p>\n<p>    async function moveTaskUp(index) {<br \/>\n        console.log(`moving todo ${index} up`);<br \/>\n        const todo = todos[index];<br \/>\n        const result = await fetch(`\/api\/Todo\/move-up\/${todo.id}`,{<br \/>\n            method: &#8220;POST&#8221;<br \/>\n        });<\/p>\n<p>        if(result.ok){<br \/>\n            await getTodo();<br \/>\n        }<br \/>\n        else{<br \/>\n            console.error(&#8216;Error moving task up:&#8217;, result.statusText);<br \/>\n        }<br \/>\n    }<\/p>\n<p>    async function moveTaskDown(index) {<br \/>\n        const todo = todos[index];<br \/>\n        const result = await fetch(`\/api\/Todo\/move-down\/${todo.id}`,{<br \/>\n            method: &#8220;POST&#8221;<br \/>\n        });<\/p>\n<p>        if(result.ok) {<br \/>\n            await getTodo();<br \/>\n        } else {<br \/>\n            console.error(&#8216;Error moving task down:&#8217;, result.statusText);<br \/>\n        }<br \/>\n    }<\/p>\n<p>    return (<br \/>\n    &lt;article<br \/>\n        className=&#8221;todo-list&#8221;<br \/>\n        aria-label=&#8221;task list manager&#8221;&gt;<br \/>\n        &lt;header&gt;<br \/>\n            &lt;h1&gt;TODO&lt;\/h1&gt;<br \/>\n                &lt;form<br \/>\n                    className=&#8221;todo-input&#8221;<br \/>\n                    onSubmit={addTask}<br \/>\n                    aria-controls=&#8221;todo-list&#8221;&gt;<br \/>\n                &lt;input<br \/>\n                    type=&#8221;text&#8221;<br \/>\n                    required<br \/>\n                    autoFocus<br \/>\n                    placeholder=&#8221;Enter a task&#8221;<br \/>\n                    value={newTaskText}<br \/>\n                    aria-label=&#8221;Task text&#8221;<br \/>\n                    onChange={handleInputChange} \/&gt;<br \/>\n                &lt;button<br \/>\n                    className=&#8221;add-button&#8221;<br \/>\n                    aria-label=&#8221;Add task&#8221;&gt;<br \/>\n                    Add<br \/>\n                &lt;\/button&gt;<br \/>\n            &lt;\/form&gt;<br \/>\n        &lt;\/header&gt;<br \/>\n        &lt;ol id=&#8221;todo-list&#8221; aria-live=&#8221;polite&#8221; aria-label=&#8221;task list&#8221;&gt;<br \/>\n            {todos.map((task, index) =&gt;<br \/>\n                &lt;TodoItem<br \/>\n                    key={task.id}<br \/>\n                    task={task.title}<br \/>\n                    deleteTaskCallback={() =&gt; deleteTask(task.id)}<br \/>\n                    moveTaskUpCallback={() =&gt; moveTaskUp(index)}<br \/>\n                    moveTaskDownCallback={() =&gt; moveTaskDown(index)}<br \/>\n                \/&gt;<br \/>\n            )}<br \/>\n        &lt;\/ol&gt;<br \/>\n    &lt;\/article&gt;<br \/>\n    );<br \/>\n}<\/p>\n<p>export default TodoList;<\/p>\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.<br \/>\nNotice that the fetch calls prefix the route with \/api, this comes from the configuration of the proxy in vite.config.js. The moveTaskDown and moveTaskUp 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.<br \/>\nThe code from above already references this css file.<\/p>\n<p>.todo-list {<br \/>\n    background-color: #1e1e1e;<br \/>\n    padding: 1.25rem;<br \/>\n    border-radius: 0.5rem;<br \/>\n    box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.3);<br \/>\n    width: 100%;<br \/>\n    max-width: 25rem;<br \/>\n}<\/p>\n<p>.todo-list h1 {<br \/>\n    text-align: center;<br \/>\n    color: #e0e0e0;<br \/>\n}<\/p>\n<p>.todo-input {<br \/>\n    display: flex;<br \/>\n    justify-content: space-between;<br \/>\n    margin-bottom: 1.25rem;<br \/>\n}<\/p>\n<p>.todo-input input {<br \/>\n    flex: 1;<br \/>\n    padding: 0.625rem;<br \/>\n    border: 0.0625rem solid #333;<br \/>\n    border-radius: 0.25rem;<br \/>\n    margin-right: 0.625rem;<br \/>\n    background-color: #2c2c2c;<br \/>\n    color: #e0e0e0;<br \/>\n}<\/p>\n<p>.todo-input .add-button {<br \/>\n    padding: 0.625rem 1.25rem;<br \/>\n    background-color: #007bff;<br \/>\n    color: #fff;<br \/>\n    border: none;<br \/>\n    border-radius: 0.25rem;<br \/>\n    cursor: pointer;<br \/>\n}<\/p>\n<p>.todo-input .add-button:hover {<br \/>\n    background-color: #0056b3;<br \/>\n}<\/p>\n<p>.todo-list ol {<br \/>\n    list-style-type: none;<br \/>\n    padding: 0;<br \/>\n}<\/p>\n<p>.todo-list li {<br \/>\n    display: flex;<br \/>\n    justify-content: space-between;<br \/>\n    align-items: center;<br \/>\n    padding: 0.625rem;<br \/>\n    border-bottom: 0.0625rem solid #333;<br \/>\n}<\/p>\n<p>.todo-list li:last-child {<br \/>\n    border-bottom: none;<br \/>\n}<\/p>\n<p>.todo-list .text {<br \/>\n    flex: 1;<br \/>\n}<\/p>\n<p>.todo-list li button {<br \/>\n    background: none;<br \/>\n    border: none;<br \/>\n    cursor: pointer;<br \/>\n    font-size: 1rem;<br \/>\n    margin-left: 0.625rem;<br \/>\n    color: #e0e0e0;<br \/>\n}<\/p>\n<p>.todo-list li button:hover {<br \/>\n    color: #007bff;<br \/>\n}<\/p>\n<p>.todo-list li button.delete-button {<br \/>\n    color: #ff4d4d;<br \/>\n}<\/p>\n<p>.todo-list li button.up-button,<br \/>\n.todo-list li button.down-button {<br \/>\n    color: #4caf50;<br \/>\n}<\/p>\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<br \/>\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<p>import { StrictMode } from &#8216;react&#8217;<br \/>\nimport { createRoot } from &#8216;react-dom\/client&#8217;<br \/>\nimport &#8216;.\/index.css&#8217;<br \/>\nimport App from &#8216;.\/App.jsx&#8217;<\/p>\n<p>createRoot(document.querySelector(&#8216;main&#8217;)).render(<br \/>\n  &lt;StrictMode&gt;<br \/>\n    &lt;App \/&gt;<br \/>\n  &lt;\/StrictMode&gt;,<br \/>\n)<\/p>\n<p>Open App.jsx and replace the content with the following.<\/p>\n<p>import TodoList from &#8220;.\/components\/TodoList&#8221;<\/p>\n<p>function App() {<br \/>\n    return (<br \/>\n        &lt;TodoList \/&gt;<br \/>\n    )<br \/>\n}<\/p>\n<p>export default App<\/p>\n<p>Open index.css and replace the contents with the CSS below.<\/p>\n<p>:root {<br \/>\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;<br \/>\n  line-height: 1.5;<br \/>\n  font-weight: 400;<\/p>\n<p>  color-scheme: light dark;<br \/>\n  color: rgba(255, 255, 255, 0.87);<br \/>\n  background-color: #242424;<br \/>\n}<br \/>\nbody {<br \/>\n    font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;<br \/>\n    background-color: #121212;<br \/>\n    color: #e0e0e0;<br \/>\n    margin: 0;<br \/>\n    padding: 0;<br \/>\n    display: flex;<br \/>\n    justify-content: center;<br \/>\n    align-items: center;<br \/>\n    height: 100vh;<br \/>\n}<\/p>\n<p>Finally, update the content of index.html to have the content below<\/p>\n<p>&lt;!doctype html&gt;<br \/>\n&lt;html lang=&#8221;en&#8221;&gt;<br \/>\n  &lt;head&gt;<br \/>\n    &lt;meta charset=&#8221;UTF-8&#8243; \/&gt;<br \/>\n    &lt;link rel=&#8221;icon&#8221; type=&#8221;image\/svg+xml&#8221; href=&#8221;\/checkmark-square.svg&#8221; \/&gt;<br \/>\n    &lt;meta name=&#8221;viewport&#8221; content=&#8221;width=device-width, initial-scale=1.0&#8243; \/&gt;<br \/>\n    &lt;title&gt;TODO app&lt;\/title&gt;<br \/>\n    &lt;link href=&#8221;https:\/\/fonts.googleapis.com\/css?family=Inter&#8221; rel=&#8221;stylesheet&#8221;&gt;<br \/>\n    &lt;script defer type=&#8221;module&#8221; src=&#8221;\/src\/main.jsx&#8221;&gt;&lt;\/script&gt;<br \/>\n  &lt;\/head&gt;<br \/>\n    &lt;body&gt;<br \/>\n      &lt;main&gt;&lt;\/main&gt;<br \/>\n    &lt;\/body&gt;<br \/>\n&lt;\/html&gt;<\/p>\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\n<div class=\"wp-video\"><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><\/div>\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\u2019t 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>.<br \/>\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.<br \/>\nWe won\u2019t 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 dotnet scaffold 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<br \/>\nissues is <a href=\"https:\/\/github.com\/dotnet\/scaffolding\">dotnet\/Scaffolding<\/a>. Feedback related to C# Dev Kit can go to<br \/>\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<br \/>\nexpressing your support. This will enable us to produce more posts of a similar nature.<\/p>\n<p>The post <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/new-aspire-app-with-react\/\">Building a Full-Stack App with React and Aspire: A Step-by-Step Guide<\/a> appeared first on <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\">.NET Blog<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>In this post we will build a new we will build a TODO app from start to finish, using Aspire [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[7],"tags":[],"class_list":["post-2311","post","type-post","status-publish","format-standard","hentry","category-dotnet"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/2311","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=2311"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/2311\/revisions"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=2311"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=2311"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=2311"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}