Skip to content
Docs Try Aspire
Docs Try

ASP.NET Core Minimal API + PostgreSQL + Vite

Aspire sample
TypeScript AppHost

Clone, run, and explore this sample

Todo app with C# Minimal APIs, Entity Framework Core, PostgreSQL, and React TypeScript frontend.

C#DashboardDatabasesDockerEF CoreNode.jsPostgreSQLTypeScript
AppHost

The entry point that composes every resource and dependency in this sample's distributed application.

View on GitHub
apphost.mts
import { ContainerLifetime, UrlDisplayLocation, createBuilder } from "./.aspire/modules/aspire.mjs";
const builder = await createBuilder();
const dc = await builder.addDockerComposeEnvironment("dc");
const postgres = await builder.addPostgres("postgres")
.withComputeEnvironment(dc)
.withDataVolume()
.withLifetime(ContainerLifetime.Persistent)
.withPgAdmin({
configureContainer: async (pgAdmin) =>
{
await pgAdmin.withLifetime(ContainerLifetime.Persistent);
}
});
const db = await postgres.addDatabase("db");
const api = await builder.addCSharpApp("api", "./api")
.withHttpHealthCheck({ path: "/health" })
.withExternalHttpEndpoints()
.withComputeEnvironment(dc)
.waitFor(db)
.withReference(db)
.withUrlForEndpoint("http", async (url) =>
{
url.displayLocation = UrlDisplayLocation.DetailsOnly;
})
.withUrlForEndpoint("https", async (url) =>
{
url.displayLocation = UrlDisplayLocation.DetailsOnly;
})
.withUrls(async (ctx) =>
{
const endpoint = ctx.getEndpoint("https");
const urls = await ctx.urls();
await urls.addForEndpoint(endpoint, `${await endpoint.url()}/scalar`, { displayText: "API Reference" });
})
.publishAsDockerComposeService(async (_, service) =>
{
await service.restart.set("always");
});
const frontend = await builder.addViteApp("frontend", "./frontend")
.withReference(api)
.withUrl("", { displayText: "Todo UI" })
.withBrowserLogs();
await api.publishWithContainerFiles(frontend, "wwwroot");
await builder.build().run();

Todo app with C# Minimal APIs, Entity Framework Core, PostgreSQL, and React TypeScript frontend.

Run Mode:

flowchart LR
    Browser --> Vite[Vite Dev Server<br/>HMR enabled]
    Vite -->|Proxy /api| API[Minimal API]
    API --> PostgreSQL

Publish Mode:

flowchart LR
    Browser --> API[Minimal API serving<br/>Vite build output<br/>'npm run build']
    API --> PostgreSQL
  • addCSharpApp: ASP.NET Core Minimal API with EF Core
  • addViteApp: React + TypeScript frontend with Vite
  • addPostgres: PostgreSQL database with pgAdmin
  • publishWithContainerFiles: Frontend embedded in API's wwwroot for publish mode
  • AddNpgsqlDbContext: Automatic PostgreSQL connection injection
  • withUrlForEndpointFactory: Custom dashboard links (Scalar API docs, Todo UI)
  • waitFor: Ensures PostgreSQL starts before API
Terminal window
aspire run

This sample is intentionally small and demo-focused. The todo CRUD endpoints under /api/todos are public and unauthenticated, and the app does not add authentication, authorization, CSRF protection, or rate limiting. GET /api/todos applies bounded pagination with offset and limit query parameters, defaulting to 50 items and capping responses at 100 items.

The PostgreSQL pgAdmin container is included as local demo tooling. Do not expose pgAdmin or these unauthenticated CRUD endpoints directly in production.

For production deployments, review the ASP.NET Core security overview, add appropriate authentication and authorization, keep request validation in place using ASP.NET Core minimal API parameter validation, add rate limiting, use antiforgery APIs when browser clients rely on cookies, and evaluate the OWASP API Security Top 10.

Terminal window
aspire run # Run locally
aspire deploy # Deploy to Docker Compose
aspire do docker-compose-down-dc # Teardown deployment

Container Files Publishing - Frontend embedded in API's wwwroot:

const postgres = await builder.addPostgres("postgres")
.withDataVolume()
.withLifetime(ContainerLifetime.Persistent)
.withPgAdmin({
configureContainer: async (pgAdmin) =>
{
await pgAdmin.withLifetime(ContainerLifetime.Persistent);
}
});
const db = await postgres.addDatabase("db");
const api = await builder.addCSharpApp("api", "./api")
.withHttpHealthCheck({ path: "/health" })
.withExternalHttpEndpoints()
.waitFor(db)
.withReference(db)
.withUrlForEndpointFactory("https", async (endpoint) => ({
url: `${await endpoint.url.get()}/scalar`,
displayText: "API Reference"
}));
const frontend = await builder.addViteApp("frontend", "./frontend")
.withReference(api)
.withUrl("", { displayText: "Todo UI" });
await api.publishWithContainerFiles(frontend, "wwwroot");

EF Core Integration - Automatic connection string injection:

builder.AddNpgsqlDbContext<TodoDbContext>("db");

Static File Serving - API serves embedded frontend in publish mode:

app.UseFileServer(); // Serves wwwroot/index.html and assets
Preview

Sample screenshots

Select the image to zoom in.

Screenshot of the Todo List UI for the ASP.NET Core Minimal API + PostgreSQL + Vite sample
Screenshot of the Todo List UI for the ASP.NET Core Minimal API + PostgreSQL + Vite sample