Setup an ASP.NET Core API + React client

Setup an ASP.NET Core API + React client

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:
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:

// /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.
👩🏻‍💻Create a new DatabaseContext.cs file in your Models directory to manage your database connection.
using Microsoft.EntityFrameworkCore;

namespace MyApp.Models;

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

  public DbSet<Todo> Todos => Set<Todo>();

  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
    modelBuilder.Entity<Todo>()
        .Property(e => e.CreatedAt)
        .HasDefaultValueSql("now()");
  }
}
👩🏻‍💻Install the Npgsql.EntityFrameworkCore.PostgreSQL package:
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
👩🏻‍💻Add the following to your Program.cs file:
using Microsoft.EntityFrameworkCore;
using MyApp.Models;

//...

var connectionString = "Host=localhost;Database=yourDBName;Username=yourUsername;Password=yourPassword";
services.AddDbContext<DatabaseContext>(
    opt =>
    {
      opt.UseNpgsql(connectionString);
      if (builder.Environment.IsDevelopment())
      {
        opt
          .LogTo(Console.WriteLine, LogLevel.Information)
          .EnableSensitiveDataLogging()
          .EnableDetailedErrors();
      }
    }
);

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 🤷‍♀️

dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
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 or Azure 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:
dotnet add package DotNetEnv
👩🏻‍💻Create a .env file in the root of your project and add your environment variables:
DATABASE_CONNECTION_STRING="your database connection string"
👩🏻‍💻Add the following to your Program.cs file:
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!

👩🏻‍💻Add GET, POST, PUT, and DELETE methods to the API to CRUD your resource.
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();
});

You can test the API using a tool like Postman, 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
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:
// 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:

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:
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.

Find an issue with this page? Fix it on GitHub