Demo project for bespoked-bikes.
# Start all services (frontend, backend, database)
docker-compose up --build
# Stop all services
docker-compose down
# View logs
docker-compose logs -f backend
docker-compose logs -f frontend
docker-compose logs -f postgres
# Connect to PostgreSQL
docker exec -it bespokedbikes-postgres psql -U bespokedbikes -d bespokedbikes
# To inspect the postgres volume:
docker volume inspect bespoked-bikes_postgres-dataFrontend: http://localhost:3000 Backend API: http://localhost:8080 API Documentation: http://localhost:8080/scalar/v1 Health Check: http://localhost:8080/health
cd apps/backend
dotnet restore
dotnet buildThe API is defined in openapi.yaml.
cd BespokedBikes.Api
dotnet runThe API will start on https://localhost:7150 (or configured port).
The API can be viewed at https://localhost:7150/scalar/v1 when running locally.
# Run all tests
dotnet test
# individual suites
dotnet test BespokedBikes.Tests.Unit
dotnet test BespokedBikes.Tests.IntegrationThis project uses GitHub Actions for continuous integration and delivery.
The CI/CD pipeline runs on every pull request to main or develop branches and executes the following jobs in parallel:
- Backend Unit Tests: Runs unit tests for the backend
- Backend Integration Tests: Runs integration tests for the backend
- Frontend Unit Tests: Placeholder for frontend unit tests (when frontend is added)
- Frontend Integration Tests: Placeholder for frontend integration tests (when frontend is added)
- System Tests: Runs after all component tests complete (currently stubbed for container setup)
.github/workflows/pr-ci.yml: Main PR workflow that orchestrates all test jobs.github/workflows/backend-test.yml: Reusable workflow for backend testing (reduces duplication)
The pipeline uses reusable workflows to avoid code duplication. The backend test workflow accepts a test-type parameter (unit or integration) and is called by the main PR workflow twice - once for each test type.
- Frontend test jobs will be populated when the frontend is implemented
- System tests will be expanded once Docker containers are set up
- Additional jobs can be added for linting, code coverage, and deployment
This project uses NSwag to generate controllers and DTOs from the OpenAPI specification. Run this whenever you update openapi.yaml:
cd apps/backend
nswag run nswag.jsonThis generates:
- Controllers:
BespokedBikes.Api/Controllers/GeneratedControllerBase.cs - DTOs:
BespokedBikes.Application/Generated/Dtos.cs
The project includes an HTTP client file (apps/backend/BespokedBikes.Api/BespokedBikes.Api.http) containing pre-written HTTP requests for testing the API endpoints. This file uses environment variables defined in apps/backend/BespokedBikes.Api/http-client.env.json.
To use it:
- Ensure the API is running (see "Run the API" section)
- Open
BespokedBikes.Api.httpin your IDE or text editor - The file supports variables like
{{BespokedBikes.Api_HostAddress}}which are resolved fromhttp-client.env.json - Execute requests directly from your IDE (most popular IDEs support HTTP file execution)
This provides a quick way to test API functionality without building a full frontend or using external tools like Postman.
This project uses Conventional Commits for commit messages.
This backend project follows Clean Architecture with a feature-oriented organization. This is a quick jumble of words trying to describe why its this "complex". I'm usually bad about remembering these terms when discussing the why so now I cant forget.
Domain (entities, enums) → no dependencies
↑
Application (services, DTOs, interfaces) → depends on Domain
↑
Infrastructure (repositories, DbContext) → depends on Domain + Application
↑
Api (controllers, middleware) → depends on Application + Infrastructure
Achieves SOLID principles:
| Principle | Implementation |
|---|---|
| S - Single Responsibility | Each project/class has one job |
| O - Open for extension/Closed for Modification | Extend via interfaces/inheritance |
| L - Liskov Substitution | IEmployeeRepository substitutable with any implementation |
| I - Interface Segregation | Small, focused interfaces (IEmployeeRepository, ISalesService) |
| D - Dependency Inversion | Application defines interfaces, Infrastructure implements them |
- .NET 9 - Web API framework
- Entity Framework Core 9.0 - ORM for database access
- PostgreSQL - Database (via Npgsql.EntityFrameworkCore.PostgreSQL 9.0.2)
- AutoMapper 12.0.1 - Object-to-object mapping
- FluentValidation 11.10.0 - Request validation
- Microsoft.Extensions.Logging - Built-in logging with console output
- OpenAPI / Scalar - API documentation (instead of Swagger)
- NUnit - Testing framework
- FluentAssertions - Assertion library for tests
- Customer
- Employee
- Product
- Sale
- Inventory
Commission percentage is stored on the Product, but the calculated commission amount is stored on each Sale. This preserves historical accuracy when commission rates change over time.
- Commission Calculation:
CommissionAmount = (SalePrice - CostPrice) × CommissionPercentage - Commission is stored on each Sale for historical accuracy
- SalePrice can differ from RetailPrice (discounts, negotiations)
- Inventory is reduced when sales are created
Products have a RetailPrice, but each Sale has a SalePrice. This allows for discounts, promotions, and price negotiations without affecting the base product pricing.
Instead of organizing by technical layers (Controllers, Services, DTOs in separate folders), we organize by business features. Each feature folder contains all related files, making it easier to understand and modify a specific feature.
(I let this fall away a bit in some of the other layers because it kind of made more sense in some places than others...)
OpenAPI specification defines the API contract, and Scalar provides a modern, beautiful documentation UI. Controllers will be generated from the OpenAPI spec.
We're using attributes which keeps boilerplate as simple as possible. There are some places this isn't working currently that should be resolved when we clean up the DTO form the service layer and the data layer schema leaking upward.
I don't like the current controller layout, I would like to have individual controller files with the routes defined there. I was wrestling with nswag a little too much getting that running and moved on since it's really just organizational.
- better layout in scalar per controller
- better folder organization
- possibly more annoying DI setup
We're using DateTimeOffset instead of DateTime to handle date and time values. This ensures that we have a consistent representation of time across different time zones and avoids issues with daylight saving time.
I discovered some of the way through that I had picked DateTime for the formats and should likely be using DateTimeOffset. I found it before we got to the Sale objects which will have some time values in the API contract. It just makes it a lot easier to work across time zones with practically zero overheads.
Refit let me generate a client straight from the open API doc and validate things like middleware behavior easily.
we strayed form the original plan a good bit when it took me longer than expected to get the basic structure in place.
the first good tests i added were right after supposedly getting to a mvp. it showed that there is actually a bug.
- scope of deliverables
- tech stack decision
- architecture design
- initial sketches
- data model
- user stories
- api design
- project plan
- setup project structure / monorepo
- initial test projects with single passing test for each
- containerization with docker
- setup ci/cd pipeline
- start development
- Implement entity classes with full properties
- Implement DTOs with full properties
- Generate controllers from OpenAPI spec (using code generator)
- Implement service business logic
- Implement repository data access
-
Add EF Core migrationsAdd fluentmigrator migrations - Add seed data for demonstration
- Implement FluentValidation rules
- Configure AutoMapper profiles
- Add middleware for global exception handling
- Docker containerization
- CI/CD pipeline setup
- Implement unit tests
- focus on areas with more logic, not the boilerplate
- get your edge cases here
- Implement integration tests
- {feature}IntegrationTests (HTTP → Full stack)
- quick and dirty test for creating a sale
- a happy path test for each feature
- {feature}RepositoryTests (Repository → Database)
- focus on where there is logic
- {feature}ControllerTests (Controller → Service)
- mock the service layer and focus on error handling middleware
- {feature}IntegrationTests (HTTP → Full stack)
- Implement end-to-end system tests using playwright and the front end.
Integration Tests (4 failing, 1 passing):
- ✅
CustomerIntegrationTests.CreateCustomer_ThenList_ShouldReturnCustomer- PASSING - ❌
EmployeeIntegrationTests.CreateEmployee_ThenList_ShouldReturnEmployee- 400 Bad Request- Root Cause: Model validation not properly configured in API pipeline
- Required Fix: Add
services.AddControllers().AddNewtonsoftJson()to Program.cs
- ❌
ProductIntegrationTests.CreateProduct_ThenList_ShouldReturnProduct- 500 Internal Server Error- Root Cause: AutoMapper configuration issue with decimal<->string conversions
- Required Fix: Verify ProductMappingProfile has correct conversions for CostPrice, RetailPrice, CommissionPercentage
- ❌
SalesIntegrationTests.SaleCreation_HappyPath- 400 Bad Request (fails at employee creation)- Root Cause: Same as #2 - will pass once employee validation is fixed
- ❌
SalesIntegrationTests.CreateSale_ThenGetSales_ShouldReturnSale- 400 Bad Request (fails at employee creation)- Root Cause: Same as #2 - will pass once employee validation is fixed
Summary: Tests are legitimate failures indicating missing application configuration, not test issues.
- we shouldnt be using the dto in the service layer. we should be doing that mapping in the api layer. when you go back to fix this make sure to wind up with something using more annotations and less strict mapping classes, not less
- we need to get to a real db asap
- the middleware needs to be setup, your exposing stack traces on errors RN
- TESTS!!!
- ODATA for querying would be a quick win to add filtering/paging/sorting
- Inventory update is kind of ugly. maybe use a JSON PATCH model.
- we need to finish cleaning up the createddate/modifieddate handling adn remove it form the dto's
- im not sold on the infastructure/domain/application layering. its making it really hard to review changes wholistically. the separation of concerns is great, but for speedrunning through this exercise its slowing me down and making me miss things in review.
- we arent return IActionResult from controllers, we should be doing that to have more control over status codes
- we should be using a healthcheck framework(middleware?) and reporting actual service health for the backend container
- nswag to go from openapi spec to controllers quickly. make sure to generate dto types even thoguht hey may "not be needed" yet becasue its annoying to configure it without them
- monorepo structure to have a silly simple UI if we have time. apps/api, apps/web
- fluentmigrator for database migrations, i like their simplicity for this project
- entity framework core with code first approach for data layer. its quick to get going and easy to seed data for demo purposes
- sqlite for database, easy to setup and demo with minimal fuss
- dotnet 9
- react with jsx for frontend, minimal setup with vite
- refit and refitter for integration tests of the api without a frontend
- nunit for tests, fluentassertions
- docker for containerization
- github actions for ci/cd
- scalar instead of swagger... its prettier
- automapper for mapping between entities and dtos
- jest for unit tests on frontend
- "happy path" system tests with playwright if we have time
- exception middleware for error handling
- repository pattern for data access abstraction
- dependency injection for service management
- logging with microsoft.extensions.logging, json file logging provider
- instrumentation with opentelemetry, console exporter for now
- authorization that utilizes employee role for access
- identityserver for auth if we have time
- ODATA for querying if we have time
initial entity/class diagrams, they may evolve as we go. class-diagram.md entity-relationship-diagram.md
the scenario doc/s were created to use as a starting point and an exercise to validate the initial entity/class designs. They may not reflect the final design decisions. user-scenario-diagram.md
I will be using AI tooling as an accelerator tool. for planning i wil use it to generate the actual diagram files form my sketches and then use it to iterate on those until they are the way I want them. during development I will use it to help me with boilerplate code generation and to help me think through problems or issues I encounter. i will not use it to generate large chunks of code or features without my direct involvement. iw ill do my best to identify areas that not my own if there are any and indicate which decisions were made with AI assistance.
i will use the commit history to "tell a story" more than anything since this is an interview exercise.