Starter ASP.NET Core Web API built on .NET 9, Entity Framework Core, and PostgreSQL.
The repository is organized as a small layered backend template with:
- API, application, domain, and infrastructure project boundaries
- consistent JSON response envelopes for success, validation, pagination, and errors
- PostgreSQL + EF Core wiring with migrations
- health checks and centralized exception handling
- integration and domain tests
src/
├── Platform.Api # HTTP entrypoint, controllers, contracts, exception handling
├── Platform.Application # Application abstractions and service registration
├── Platform.Domain # Domain entities and rules
└── Platform.Infrastructure # EF Core, DbContext, migrations, database options
tests/
├── Platform.Api.Tests
└── Platform.Application.Tests
The project currently exposes these endpoints:
GET /GET /healthGET /api/system/infoPOST /api/system/echoGET /api/system/paged-sample?page=1&pageSize=2GET /api/system/throw/{kind}
{kind} supports:
validationnotfoundconflictforbidden- any other value returns a simulated
500
OpenAPI is enabled in development. The raw OpenAPI document is exposed at /openapi/v1.json, and interactive Scalar docs are available at /docs.
The sample domain currently includes a WorkItem entity with:
- trimmed title and description handling
- audit timestamps from
AuditableEntity - completion state tracking
- EF Core mapping to the
work_itemstable
.NET SDK 9.0.306or compatible9.0.x- PostgreSQL 16
- Docker (optional, for running PostgreSQL in a container)
The SDK version pinned in global.json is 9.0.306.
Run the automated setup script to configure everything:
# First time: restore dotnet tools
dotnet tool restore
# Run setup script
dotnet script scripts/setup.csxThe script will:
- Auto-detect existing
.envfile and use its values, or prompt you if missing - Check prerequisites (.NET SDK, Docker)
- Generate secure JWT secret key if needed (auto-updates
.env) - Configure .NET User Secrets with your values
- Create
.envfile for Docker Compose if it doesn't exist - Start PostgreSQL via Docker Compose (if Docker available)
- Apply database migrations
After setup completes, start the API:
dotnet run --project src/Platform.Api/Platform.Api.csprojIf you prefer to configure manually:
Using Docker:
docker compose up postgres -dOr use your local PostgreSQL installation.
cd src/Platform.Api
# Set database connection string
dotnet user-secrets set "ConnectionStrings:DefaultConnection" \
"Host=localhost;Port=5432;Database=platform_api;Username=postgres;Password=YOUR_PASSWORD"
# Set JWT configuration (secret key must be at least 32 characters)
dotnet user-secrets set "Jwt:SecretKey" "your-secret-key-minimum-32-characters"
dotnet user-secrets set "Jwt:Issuer" "Platform.Api"
dotnet user-secrets set "Jwt:Audience" "Platform.Api"
dotnet user-secrets set "Jwt:AccessTokenExpirationMinutes" "60"
dotnet user-secrets set "Jwt:RefreshTokenExpirationDays" "7"./scripts/migrate.sh applyOr using the Makefile:
make migrate applydotnet run --project src/Platform.Api/Platform.Api.csprojTo run the complete stack with Docker:
# Copy and configure environment file
cp .env.example .env
# Edit .env and set your values
# Start all services
docker compose upThe API will be available at http://localhost:5000.
The API uses standard ASP.NET Core configuration precedence with fail-fast validation:
- Local development: Uses .NET User Secrets for sensitive values (connection strings, JWT secrets)
- Docker: Uses
.envfile for Docker Compose variable interpolation - Base configuration:
appsettings.jsonandappsettings.Development.jsoncontain only non-sensitive defaults
The app validates required configuration at startup and fails with clear error messages if values are missing.
To view configured user secrets:
dotnet user-secrets list --project src/Platform.ApiLocal development URLs:
http://127.0.0.1:5000https://127.0.0.1:5001
API documentation (development only):
http://127.0.0.1:5000/docshttps://127.0.0.1:5001/docs- Raw OpenAPI JSON:
/openapi/v1.json
dotnet restore Platform.Api.sln
dotnet build Platform.Api.sln -m:1 -nr:falseOr using make:
make restore
make buildCreate a new migration:
./scripts/migrate.sh create YourMigrationName
# Or: make migrate NAME=YourMigrationNameApply migrations:
./scripts/migrate.sh apply
# Or: make migrate applyThe API uses a shared envelope with these top-level fields:
successmessagedatapageMetaerrorstraceId
Top-level nullable fields are omitted from the serialized JSON when their value is null. In practice:
- success responses usually include
data - paginated responses include both
dataandpageMeta - validation responses include
errors - unhandled server errors include
traceId
Example success response:
{
"success": true,
"message": "System information retrieved successfully.",
"data": {
"service": "Platform.Api",
"status": "ready",
"utc": "2026-03-31T12:00:00+00:00"
}
}Example validation response:
{
"success": false,
"message": "Validation failed.",
"errors": [
{
"field": "Message",
"messages": [
"The Message field is required."
]
}
]
}Example paginated response:
{
"success": true,
"message": "Paged sample retrieved successfully.",
"data": [
{
"id": 4,
"name": "Item 4"
}
],
"pageMeta": {
"page": 2,
"pageSize": 3,
"totalItems": 10,
"totalPages": 4
}
}Example unhandled error response:
{
"success": false,
"message": "An unexpected error occurred.",
"traceId": "0HNABCDE12345:00000002"
}Unhandled exceptions are translated centrally in src/Platform.Api/Program.cs:
ValidationException->400 Bad RequestKeyNotFoundException->404 Not FoundInvalidOperationException->409 ConflictUnauthorizedAccessException->403 Forbidden- any other unhandled exception ->
500 Internal Server Error
404 responses from unknown routes are also wrapped in the same JSON error envelope.
Run all tests with dotnet:
dotnet test Platform.Api.sln -m:1 -nr:falseOr with make:
make testCurrent tests cover:
- root endpoint response shape
- health endpoint response shape
- wrapped success responses
- paginated responses
- validation failures
- unknown routes
- exception-to-status-code mapping
WorkItemdomain behavior
Because the API always wires PostgreSQL and a database health check during startup, local test execution expects a valid configured connection string and a reachable PostgreSQL instance unless the test host setup is changed.