--- type: article title: "Setup an ASP.NET Core API + React client" date: "2023-02-08T23:23:28.003Z" slug: "asp-net-core-api-react-client" image: name: "nET.png" width: 1280 height: 720 status: "published" description: "How to setup up a new ASP.NET Core API with a database and a React client." tags: ["c#", "csharp", "dotnet", ".NET", "asp.net", "react"] --- This is more of a reference than a how to guide. It goes through the steps of setting up a new ASP.NET Core API with a database, but there's not much information on why you would do things a certain way. ## Setup Create a new empty web app: ```bash dotnet new web -o MyApp ``` If you want controller's you can add them in the endpoints section. ## Database and Models Create a `Models` directory with a model class for your resource. For example, if you were creating a `Todo` list application, you might name your model file `Todo.cs` and it would look like this: ```csharp // /Models/Todo.cs namespace MyApp.Models; public class Todo { public int Id { get; set; } public string Title { get; set; } public bool Completed { get; set; } public DateTime CreatedAt { get; set; } } ``` Choose either Postgres, MySQL, or In Memory for your database. {/* prettier-ignore */} Create a new `DatabaseContext.cs` file in your `Models` directory to manage your database connection. ```csharp using Microsoft.EntityFrameworkCore; namespace MyApp.Models; public class DatabaseContext : DbContext { public DatabaseContext(DbContextOptions options) : base(options) { } public DbSet Todos => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .Property(e => e.CreatedAt) .HasDefaultValueSql("now()"); } } ``` Install the `Npgsql.EntityFrameworkCore.PostgreSQL` package: ```bash dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL ``` Add the following to your `Program.cs` file: ```csharp using Microsoft.EntityFrameworkCore; using MyApp.Models; //... var connectionString = "Host=localhost;Database=yourDBName;Username=yourUsername;Password=yourPassword"; services.AddDbContext( opt => { opt.UseNpgsql(connectionString); if (builder.Environment.IsDevelopment()) { opt .LogTo(Console.WriteLine, LogLevel.Information) .EnableSensitiveDataLogging() .EnableDetailedErrors(); } } ); ``` Create a new `DatabaseContext.cs` file in your `Models` directory to manage your database connection. ```csharp using Microsoft.EntityFrameworkCore; namespace MyApp.Models; public class DatabaseContext : DbContext { public DatabaseContext(DbContextOptions options) : base(options) { } public DbSet Todos => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .Property(e => e.CreatedAt) .HasDefaultValueSql("now()"); } } ``` Install the `Pomelo.EntityFrameworkCore.MySql` package: ```bash dotnet add package Pomelo.EntityFrameworkCore.MySql --version 7.0.0 ``` Add the following to your `Program.cs` file: ```csharp var connectionString = "server=localhost;user=root;password=1234;database=my_db"; var serverVersion = new MySqlServerVersion(new Version(8, 0, 29)); builder.Services.AddDbContext( opt => { opt .UseMySql(connectionString, serverVersion) if (builder.Environment.IsDevelopment()) { opt .LogTo(Console.WriteLine, LogLevel.Information) .EnableSensitiveDataLogging() .EnableDetailedErrors(); } } ); ``` {" "} Create a new `DatabaseContext.cs` file in your `Models` directory to manage your database connection. ```csharp using Microsoft.EntityFrameworkCore; namespace MyApp.Models; public class DatabaseContext : DbContext { public DatabaseContext(DbContextOptions options) : base(options) { } public DbSet Todos => Set(); } ``` Install the `Microsoft.EntityFrameworkCore.InMemory` package: ```bash dotnet add package Microsoft.EntityFrameworkCore.InMemory ``` Add the following to your `Program.cs` file: ```csharp builder.Services.AddDbContext( opt => { opt.UseInMemoryDatabase("InMemoryDatabase"); if (builder.Environment.IsDevelopment()) { opt .LogTo(Console.WriteLine, LogLevel.Information) .EnableSensitiveDataLogging() .EnableDetailedErrors(); } } ); ``` If you're using the InMemory database, you don't need to worry about the env var or migration sections below. Optionally, you could also add the `Diagnostics` package to get some nice error pages when things go wrong. I haven't seen this work yet, but the docs suggest using it so 🤷‍♀️ ```bash dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore ``` ```csharp if (builder.Environment.IsDevelopment()) { builder.Services.AddDatabaseDeveloperPageExceptionFilter(); } ``` ## Environment Variables This isn't really document well anywhere and seems to go against how the .NET community likes to store configuration values. However, here's my argument for using normal environment variables: 1. In production, we're supposed to use a secure secret manager like [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) or [Azure Key Vault](https://azure.microsoft.com/en-us/services/key-vault/). These services are designed to store secrets and are much more secure than environment variables, or configuration files. 2. In development, we're not really concerned with security as much, so it doesn't really matter how we store our secrets as long as they are "fake" secrets. Like the root password to my local MySQL database isn't really sensitive information in any way. 3. Environment variables are commonly used, so most devs are familiar with them. If we're sharing code with other devs, and developing on multiple machines, it's easier to just use environment variables for secrets and configuration to avoid updating source control tracked files for each user's confiruations. Install the `DotNetEnv` package: ```bash dotnet add package DotNetEnv ``` Create a `.env` file in the root of your project and add your environment variables: ```bash DATABASE_CONNECTION_STRING="your database connection string" ``` Add the following to your `Program.cs` file: ```csharp DotNetEnv.Env.Load(); var connectionString = Environment.GetEnvironmentVariable("DATABASE_CONNECTION_STRING"); ``` ## Migrations Make sure you have the `dotnet-ef` tool installed on your machine: ``` dotnet tool install --global dotnet-ef ``` Install the `Microsoft.EntityFrameworkCore.Design` package: ``` dotnet add package Microsoft.EntityFrameworkCore.Design ``` Create a new migration: ``` dotnet ef migrations add InitialCreate dotnet ef database update ``` ## CRUD Endpoints Once you have your database setup, you can start creating CRUD endpionts. This is the fun part! {/* prettier-ignore */} Add GET, POST, PUT, and DELETE methods to the API to CRUD your resource. ```csharp app.MapGet("/api/todoitems", async (DatabaseContext db) => await db.Todos.ToListAsync()); app.MapGet("/api/todoitems/complete", async (DatabaseContext db) => await db.Todos.Where(t => t.Completed).ToListAsync()); app.MapGet("/api/todoitems/{id}", async (int id, DatabaseContext db) => await db.Todos.FindAsync(id) is Todo todo ? Results.Ok(todo) : Results.NotFound()); app.MapPost("/api/todoitems", async (Todo todo, DatabaseContext db) => { db.Todos.Add(todo); await db.SaveChangesAsync(); return Results.Created($"/api/todoitems/{todo.Id}", todo); }); app.MapPut("/api/todoitems/{id}", async (int id, Todo inputTodo, DatabaseContext db) => { var todo = await db.Todos.FindAsync(id); if (todo is null) return Results.NotFound(); todo.Name = inputTodo.Name; todo.Completed = inputTodo.Completed; await db.SaveChangesAsync(); return Results.NoContent(); }); app.MapDelete("/api/todoitems/{id}", async (int id, DatabaseContext db) => { if (await db.Todos.FindAsync(id) is Todo todo) { db.Todos.Remove(todo); await db.SaveChangesAsync(); return Results.Ok(todo); } return Results.NotFound(); }); ``` Create a Controllers folder and a `TodosController` class inside that folder: ```csharp using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MyApp.Models; namespace MyApp.Controllers; [ApiController] [Route("api/[controller]")] public class TodosController : ControllerBase { private readonly DatabaseContext _context; public TodosController(DatabaseContext context) { _context = context; } [HttpGet] public async Task>> GetTodoItems() { return await _context.Todos.ToListAsync(); } [HttpGet("complete")] public async Task>> GetCompletedTodoItems() { return await _context.Todos.Where(t => t.Completed).ToListAsync(); } [HttpGet("{id}")] public async Task> GetTodoItem(int id) { var todoItem = await _context.Todos.FindAsync(id); if (todoItem == null) { return NotFound(); } return todoItem; } [HttpPost] public async Task> PostTodoItem(Todo todo) { _context.Todos.Add(todo); await _context.SaveChangesAsync(); return CreatedAtAction(nameof(GetTodoItem), new { id = todo.Id }, todo); } [HttpPut("{id}")] public async Task PutTodoItem(int id, Todo todo) { if (id != todo.Id) { return BadRequest(); } _context.Entry(todo).State = EntityState.Modified; await _context.SaveChangesAsync(); return NoContent(); } [HttpDelete("{id}")] public async Task DeleteTodoItem(int id) { var todoItem = await _context.Todos.FindAsync(id); if (todoItem == null) { return NotFound(); } _context.Todos.Remove(todoItem); await _context.SaveChangesAsync(); return NoContent(); } } ``` Instead of manually creating the controller, you can use the `dotnet aspnet-codegenerator` tool to generate the controller for you: ```bash dotnet aspnet-codegenerator controller \ -name TodosController \ -async \ -api \ -m Todo \ -dc DatabaseContext \ -outDir Controllers ``` But first you will need to install `dotnet tool install -g dotnet-aspnet-codegenerator`, and `Microsoft.VisualStudio.Web.CodeGeneration.Design`, and and `Microsoft.EntityFrameworkCore.Tools`, and for some reason you also need `Microsoft.EntityFrameworkCore.SqlServer` even though you're using MySQL or Postgres. Add the following to your `Startup.cs` file: ```csharp using TodoApi.Controllers; // ... builder.Services.AddControllers(); // ... app.MapControllers(); ``` You can test the API using a tool like [Postman](https://www.postman.com/), but I suggest adding swagger. ## Swagger ASP.NET APIs provide built-in support for generating information about endpoints via the `Microsoft.AspNetCore.OpenApi` package. https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/openapi?view=aspnetcore-7.0 Install the following packages for swagger: ``` dotnet add package Microsoft.AspNetCore.OpenApi dotnet add package Swashbuckle.AspNetCore ``` Enable swagger in your app ```csharp using Microsoft.AspNetCore.OpenApi; // ... builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } ``` Now you can test your app and see the swagger UI at `http://localhost:5000/swagger/` ## React This react app will be deployed as part of your .NET app in production. They will exist on the same server, with the .NET app serving the react app as its frontend. Create a new React app: ``` yarn create vite my-app-frontend ``` Proxy to the dotnet server: ```js // https://vitejs.dev/config/ export default defineConfig({ server: { proxy: { "/api": "http://localhost:5053", }, }, plugins: [react()], }) ``` Implement the frontend in React. Make GET, POST, PUT, and DELETE requests to the API to CRUD your resource. Example: ```js async function createTodo(name) { const result = await fetch("/api/todos", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ name }), }).then((res) => res.json()) } ``` When the frontend is done and your application is ready to deploy, build your react app. ``` yarn build ``` Copy the `dist` folder from your react app into your .NET project and rename `dist` to `wwwroot` Add the following to your `Program.cs`: ```c# app.UseDefaultFiles(); app.UseStaticFiles(); app.MapFallbackToFile("index.html"); ``` The dotnet app should now serve up your frontend app when you make a request that isn't handled by the API. ## Other Helpful Links - https://learn.microsoft.com/en-us/aspnet/core/fundamentals/apis?view=aspnetcore-7.0 - https://learn.microsoft.com/en-us/ef/core/modeling/