Clean-architecture recipe API (ASP.NET Core 10 + EF Core + PostgreSQL) with a React 19 / Vite frontend.
RecipeManager/
RecipeManager.Domain/ entities, guard clauses
RecipeManager.Application/ use cases, validators (FluentValidation/FluentResults)
RecipeManager.Infrastructure/ EF Core DbContext, migrations
RecipeManager.Api/ controllers, DI/startup, Swagger
RecipeManager.UnitTests/ xUnit + NSubstitute
RecipeManager.IntegrationTests/ xUnit + WebApplicationFactory (EF InMemory)
recipe-manager-frontend/ React 19 + Vite + MUI
| Tool | Version | Notes |
|---|---|---|
| .NET SDK | 10.0 | All projects target net10.0. Pinned in RecipeManager/global.json with rollForward: latestFeature. |
| PostgreSQL | 16 or newer | Accessed via Npgsql. Default host/port localhost:5432. |
| Node.js | 20 LTS or newer; 24 recommended | Only needed for the frontend. engines declares the >=20 floor; recipe-manager-frontend/.nvmrc pins 24, which is what CI installs and what the project is tested on. nvm use in that folder picks it up. |
Install on Windows:
winget install Microsoft.DotNet.SDK.10winget install PostgreSQL.PostgreSQL.18 --interactivewinget install OpenJS.NodeJS.LTSOpen a new terminal afterwards so PATH picks up the new tools.
Run everything below from the RecipeManager/ folder (the one holding RecipeManager.sln).
dotnet restore RecipeManager.slndotnet build RecipeManager.slnProgram.cs applies EF migrations at startup (app.MigrateDatabase()), so the API creates and updates
the schema on first run — but it will fail to start if PostgreSQL is not reachable.
Create the role and database once (run from an elevated-enough shell; you'll be prompted for the
postgres superuser password set during install):
& "C:\Program Files\PostgreSQL\18\bin\psql.exe" -U postgres -c "CREATE ROLE recipemanager LOGIN PASSWORD 'your-password';"& "C:\Program Files\PostgreSQL\18\bin\createdb.exe" -U postgres -O recipemanager DbRecipeManagerRecipeManager.Api/appsettings.json holds a password-less template:
Host=localhost;Port=5432;Database=DbRecipeManager;Username=recipemanager;Timeout=90
Supply the password locally via user-secrets so it never reaches git (the API project already has a
UserSecretsId):
dotnet user-secrets --project RecipeManager.Api set "ConnectionStrings:DefaultConnection" "Host=localhost;Port=5432;Database=DbRecipeManager;Username=recipemanager;Password=your-password"In deployment, override the same key with the ConnectionStrings__DefaultConnection environment variable.
To apply migrations manually instead of at startup:
dotnet tool install --global dotnet-efdotnet ef database update --project RecipeManager.Infrastructure --startup-project RecipeManager.Apidotnet run --project RecipeManager.Api --launch-profile https- HTTPS:
https://localhost:7231(Swagger UI at/swagger) - HTTP:
http://localhost:5249
Trust the local HTTPS certificate once, otherwise the browser and the Vite proxy will reject it:
dotnet dev-certs https --trustdotnet test RecipeManager.slnUnit tests with an HTML coverage report (requires dotnet tool install --global dotnet-reportgenerator-globaltool):
pwsh ./run-coverage.ps1cd recipe-manager-frontendnpm installnpm run devServes on http://localhost:3000 — the origin the API's CORS policy (AllowReactApp) allows, so don't
change the port without updating RecipeManager.Api/Startup/ServiceInitializer.cs.
.env.development points VITE_API_URL at https://localhost:7231/api. Delete or blank that variable to
fall back to the relative /api path, which vite.config.ts proxies to the same backend.
Start the API first — the frontend has no mock backend.
Run these before opening a pull request; nothing runs them for you yet.
npm run lintnpm run buildnpm run build is tsc -b && vite build, so a type error fails it before Vite bundles anything. For a faster
loop while working, npm run typecheck runs the same check without producing dist/. There is no npm test —
no frontend test runner exists yet.
.github/workflows/ci.yml runs on every pull request to main and every push to main, in two parallel jobs
on ubuntu-latest:
| Job | Steps |
|---|---|
| Backend | dotnet restore --locked-mode → dotnet build (Debug) → dotnet test (84) → vulnerable-package check |
| Frontend | npm ci → npm run typecheck → npm run lint → npm run build → npm audit --audit-level=high |
Two things are worth knowing before a run surprises you:
- NuGet restores from committed lock files. Every project has a
packages.lock.json, and CI restores in locked mode. Change a package version and CI fails withNU1004until you rundotnet restore RecipeManager.sln --force-evaluateand commit the updated lock files. - CI builds Debug on purpose. A Release build makes every integration test fail: the
IntegrationTestenvironment throws in RELEASE builds by design, andWebApplicationFactoryuses that environment name. See ADR-005 and ADR-013.
The checks are not yet required to merge — that is a branch-protection setting on main.
RecipeManager.Api/Dockerfile builds the API alone (no database container). Build from the RecipeManager/
folder so the COPY paths resolve:
docker build -f RecipeManager.Api/Dockerfile -t recipemanager-api .The image needs a reachable PostgreSQL; pass the connection string via
ConnectionStrings__DefaultConnection. Note that Host=localhost resolves to the container, not your
machine — use host.docker.internal (Docker Desktop) or a compose service name instead.
You must install or update .NET to run this application / Framework 'Microsoft.NETCore.App', version 'X' not found
The solution built, but the matching runtime is missing. Install the .NET 10 SDK (above); dotnet --list-runtimes
should show a 10.x entry for both Microsoft.NETCore.App and Microsoft.AspNetCore.App.
API throws on startup with a Npgsql connection or authentication error
PostgreSQL is not running, or the credentials are wrong. Check the service with
Get-Service postgresql* in PowerShell, and confirm the password is set in user-secrets — the template in
appsettings.json deliberately has none.
relation "recipes" does not exist when querying in psql
PostgreSQL folds unquoted identifiers to lowercase, and EF creates the table as "Recipes". Quote it:
SELECT * FROM "Recipes";
Frontend requests fail with a certificate error
Run dotnet dev-certs https --trust.