A full-stack example procurement and purchase approvals system. Demonstrates real-world patterns, testing, deployment, and Infrastructure As Code using Terraform.
- C# / .Net API and domain
- React frontend
- Postgres DB
- End-to-End Type Safety: OpenAPI spec auto-generates a TypeScript client. Types flow from C# API to React components
- Vertical Slice Architecture: Features grouped cohesively on both backend and frontend
- Role-Based Access Control: UI adapts per role. API enforces authorization at every endpoint
- Comprehensive Testing: Quick integration tests for cross-cutting concerns, and detailed tests for domain logic
- Infrastructure as Code: Terraform modules for Azure deployment (Container Apps, Postgres, Key Vault)
- AI-Assisted Development:
AGENTS.mdfiles built up during development and pointing to best practice examples.
Domain
- EF Core Entity model & mapping: PurchaseRequest
- Domain handlers: ProcureHub/Features/PurchaseRequests
- Command handler: CreatePurchaseRequest.cs
- Query handler: QueryPurchaseRequests.cs
API
- Endpoint mapping: PurchaseRequests/Endpoints.cs
- Domain
IRequestHandlerinjected into endpoints - Full OpenAPI configuration to enable typesafe client generation
- Domain
Frontend
- Features folder: src/features/purchase-requests
- Routes: src/routes/(auth)/_app-layout/requests
Tests
- Integration tests: PurchaseRequestTests.cs
- Note there are 2 test classes in file - one for mostly stateless, cross-cutting concerns (database only reset per class instance), and one for detailed domain testing using Arrange-Act-Assert approach (DB reset per test).
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β React SPA (TanStack) β
β TanStack Router Β· TanStack Query Β· shadcn/ui Β· Tailwind β
βββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ
β openapi-react-query (generated client)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ASP.NET Core Minimal API β
β OpenAPI 3.1 Β· FluentValidation Β· ASP.NET Identity β
βββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ
β EF Core
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PostgreSQL β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- ASP.NET Core Minimal APIs with OpenAPI 3.1 documentation
- EF Core with PostgreSQL, code-first migrations, clean entity configuration
- ASP.NET Identity with custom application User entity
- FluentValidation with automatic decorator-based validation (ValidationRequestHandlerDecorator)
- Problem Details for consistent error responses
- TanStack Router for type-safe routing
- Typesafe API client based on TanStack Query, via openapi-react-query
- shadcn/ui component library with Tailwind CSS
- Feature-based structure mirroring backend organization
The API exposes an OpenAPI spec that generates a fully typed TypeScript client, using the openapi-react-query library. The generated client wraps TanStack Query.
npm run generate:api-schema # Regenerates client from OpenAPI specDomain request handlers return a Result<T> type, keeping domain logic decoupled from HTTP concerns. API maps to HTTP error using ToProblemDetails extension method.
// Handler returns domain result
public async Task<Result<string>> HandleAsync(Request request, CancellationToken token)
{
// ... domain logic
return Result.Success(userId);
// or: return Result.Failure<string>(UserErrors.EmailTaken);
}
// Endpoint maps to HTTP response
result.Match(
userId => Results.Created($"/users/{userId}", new { userId }),
error => error.ToProblemDetails() // Converts Error β RFC 9457 Problem Details
);Simple AddRequestHandlers extension method in the domain project:
- Registers all
IRequestHandlerimplementations - Adds a
ValidationRequestHandlerDecoratorwhich runs any FluentValidation validators before invoking the handlers
(For more complex needs, I would consider using MediatR)
Custom logic was needed to ensure nested response types generate unique schema names. See: CreateOpenApiSchemaReferenceId.
Example: Transforms DataResponse<GetUserById.Response> to "DataResponseOfGetUserByIdResponse".
The standard ASP.Net MapIdentityApi required two fixes:
- Added missing 401 response documentation for
/loginendpoint (to ensure generated client had correct types). I also added that to the Github issue - Added
/logoutendpoint (not included by default)
See: ConfigureIdentityApiEndpoints
This project is structured for effective collaboration with AI coding agents:
AGENTS.mdfiles at project root and in key directories provide context and conventions- Context documents in
.context/describe the domain and use cases - Good patterns established mostly manually first, then agents pointed to those
I use two different test classes for each feature area:
- One uses an xUnit
IClassFixtureto only reset the database once per class fixture (good for testing stateless, cross-cutting concerns like authentication and basic validation) - The other resets the database before each test (used to test the core domain logic using Arrange-Act-Assert approach)
Example: See the UserTestsWithSharedDb and UserTests classes in UserTests.cs
I also use xUnit theory tests to enforce that all endpoints have correct authentication, authorization, and basic DTO validation tests. Snippets shown below
public static TheoryData<EndpointInfo> GetAllUserEndpoints() => new()
{
new EndpointInfo("/users", "POST", "CreateUser"),
new EndpointInfo("/users", "GET", "QueryUsers"),
// ... every endpoint listed
};
[Theory]
[MemberData(nameof(GetAllUserEndpoints))]
public async Task All_user_endpoints_require_authentication(EndpointInfo endpoint)
{
// Note: Not logging in as anyone initially
var path = endpoint.Path.Replace("{id}", "test-id");
var request = new HttpRequestMessage(new HttpMethod(endpoint.Method), path);
var resp = await HttpClient.SendAsync(request);
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
}
[Theory]
[MemberData(nameof(GetAllUserEndpoints))]
public void All_user_endpoints_have_validation_tests(EndpointInfo endpoint)
{
// Verify test method exists using reflection
var testMethod = GetType().GetMethod($"Test_{endpoint.Name}_validation");
Assert.NotNull(testMethod);
}
[Fact]
public async Task Test_CreateUser_validation()
{
// Test validation on the CreateUser endpoint
...
}
[Fact]
public async Task Test_QueryUsers_validation()
{
// Test validation on the QueryUsers endpoint
...
}- Terraform modules in
/infrafor Azure resources - Uses separate
stagingandproductionenvironments
- GitHub Actions workflows for build, test, and deploy
- API containerized with Docker, pushed to GitHub Container Registry
- Frontend deployed to Azure Static Web Apps
- .NET 10 SDK
- Node.js 20+
- Docker & Docker Compose
docker compose up -dcd ProcureHub.WebApi
dotnet runcd ProcureHub.WebApp
npm install
npm run devdotnet test ProcureHub.sln
