A production-grade .NET 9 REST API that orchestrates the public Hacker News API — built around Clean Architecture, resilience pipelines, full observability, and container-first deployment.
- ⚡ High-performance orchestration of the Hacker News public API with concurrency throttling and in-memory caching
- 🧱 Clean Architecture — clear separation between
Domain,Application,Infrastructure,Common, andCore.API - 🛡️ Resilience-first — Polly v8 pipeline with retries, timeouts, and circuit breaker via
Microsoft.Extensions.Http.Resilience - 📜 OpenAPI / Swagger documentation with API versioning (
/api/v{version}/...) - 🔭 Full observability — OpenTelemetry traces/metrics + Serilog structured logging with
TraceId/SpanIdcorrelation - 🚦 Centralized error handling via
ExceptionsHandlingMiddlewarereturning RFC 7807ProblemDetails - 🧪 Tested — xUnit + NSubstitute unit tests and Stryker.NET mutation testing
- 🐳 Container-ready — multi-stage Alpine Dockerfile running as non-root on port
8080 - 🚀 CI/CD scaffold — GitHub Actions workflow with build, test, and ECS Fargate deployment stages
A single, focused endpoint:
GET /api/v1/News/top/{n}| Param | Type | Description |
|---|---|---|
n |
int (1 — MaxStoriesPerRequest) |
Number of best stories to return, sorted by score |
Sample response (200 OK):
[
{
"title": "Show HN: A Faster JSON Parser",
"uri": "https://example.com/post",
"postedBy": "pg",
"time": "2026-05-01T01:23:45Z",
"score": 482,
"commentCount": 137
}
]Error responses follow RFC 7807 ProblemDetails:
| Status | Meaning |
|---|---|
400 Bad Request |
n <= 0 or n > MaxStoriesPerRequest |
499 Client Closed Request |
Caller cancelled the request |
500 Internal Server Error |
Unhandled failure |
503 Service Unavailable |
Upstream Hacker News API unreachable / circuit open |
Interactive docs available at /swagger when running locally.
┌─────────────────────────────────────────────────────────┐
│ Core.API │
│ Controllers · Middleware · DI · Versioning · Swagger │
└────────────────────────┬────────────────────────────────┘
│ depends on
┌────────────────────────▼────────────────────────────────┐
│ Application │
│ Use cases · Orchestration · Interfaces │
│ HackerNewsService │
└──────────────┬─────────────────────────┬────────────────┘
│ │
┌──────────────▼──────────┐ ┌───────────▼────────────────┐
│ Infrastructure │ │ Domain │
│ HTTP clients · Polly · │ │ DTOs · Responses · POCO │
│ Cache · External I/O │ │ │
└─────────────────────────┘ └────────────────────────────┘
▲
│ shared utilities
┌──────┴──────┐
│ Common │
│ Config etc. │
└─────────────┘
Dependency rule: outer layers depend on inner layers — never the inverse. Domain has zero dependencies; Application defines interfaces (e.g. IHackerNewsService, IHackerNewsGateway, ICacheProvider) implemented by Infrastructure.
| Concern | Where | What |
|---|---|---|
| Resilience | Core.API/Config/ApiConfigAndResilience.cs |
Polly retry + timeout + circuit breaker on the typed HttpClient |
| Mapping | Core.API/Config/MappingConfigurations.cs |
Mapster configurations |
| Errors | Core.API/Middleware/ExceptionsHandlingMiddleware |
Catches all exceptions, emits ProblemDetails |
| Tracing | Program.cs (OpenTelemetry) |
OTLP exporter to OpenTelemetry:OtlpEndpoint |
| Logging | Program.cs (Serilog) |
Console sink, enriched with TraceId/SpanId |
- .NET 9 SDK
- Docker (optional — for containerized run)
git clone <repo-url>
cd developer-coding-test
dotnet restore
dotnet run --project Core.API/API.csprojAPI listens on the URL printed by Kestrel (e.g. https://localhost:5001).
Open /swagger for the interactive UI.
docker build -t hackernews-api .
docker run -p 8080:8080 hackernews-apiThen hit http://localhost:8080/api/v1/News/top/10.
The repository ships a docker-compose.yml that spins up the API and a Jaeger all-in-one container to receive OpenTelemetry traces:
docker compose up --build| Service | URL | Purpose |
|---|---|---|
| API | http://localhost:8080/swagger | Swagger UI |
| API | http://localhost:8080/api/v1/News/top/10 | Endpoint sample |
| Jaeger UI | http://localhost:16686 | Distributed traces |
| OTLP gRPC | localhost:4317 |
OpenTelemetry ingest |
Tear it down with:
docker compose downConfigurable via appsettings.json, environment variables, or any standard ASP.NET Core configuration source.
Override any value via environment variable using ASP.NET Core conventions, e.g.:
HackerNewsOptions__MaxStoriesPerRequest=100dotnet test Tests/UnitTests/UnitTests.csproj21 tests covering caching behavior, concurrency throttling, ordering, and error paths.
dotnet tool restore
cd Tests/MutationTests
dotnet stryker -f stryker-config.jsonGenerates an HTML report under Tests/MutationTests/StrykerOutput/.../reports/mutation-report.html.
The included Dockerfile is a multi-stage Alpine build:
buildstage —mcr.microsoft.com/dotnet/sdk:9.0-alpinerestores and publishesruntimestage —mcr.microsoft.com/dotnet/aspnet:9.0-alpine, runs as non-rootappuser, exposes8080
docker build -t hackernews-api .
docker run --rm -p 8080:8080 \
-e ASPNETCORE_ENVIRONMENT=Production \
hackernews-apiThe pipeline at .github/workflows/build-and-deploy.yml runs on every push and PR, performing:
- ✅ Restore, build, and run unit tests
- 🐳 Build and push the Docker image to Amazon ECR
- 🚀 Deploy a new task revision to AWS ECS Fargate
Provide these GitHub Action secrets to enable deployment:
| Secret | Purpose |
|---|---|
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY |
AWS credentials |
AWS_REGION, ECR_REPOSITORY, ECS_CLUSTER, ECS_SERVICE, ECS_TASK_DEFINITION |
Target infrastructure |
.
├── Core.API/ # ASP.NET Core entrypoint (Controllers, Middleware, DI, Swagger)
├── Application/ # Use cases & service orchestration
│ ├── Interfaces/
│ └── Services/ # HackerNewsService
├── Infrastructure/ # External I/O: HTTP clients, cache, gateways
├── Domain/ # DTOs, response contracts, pure domain types
├── Common/ # Cross-cutting helpers, options classes
├── Tests/
│ ├── UnitTests/ # xUnit + NSubstitute
│ └── MutationTests/ # Stryker.NET configuration
├── .github/workflows/ # CI/CD pipelines
├── Dockerfile # Multi-stage Alpine build
├── docker-compose.yml # API + Jaeger (OTLP) local stack
└── Core.sln
MIT — feel free to use, learn from, and adapt.
Built with ❤️ and a healthy dose of resilience patterns.
{ "GenericHttpClientOptions": { "BaseUrl": "https://hacker-news.firebaseio.com/v0", "TimeoutSeconds": 30 }, "HackerNewsOptions": { "MaxStoriesPerRequest": 50, // Hard cap on `n` "MaxConcurrentStoryRequests": 10, // Concurrency throttle for fan-out "BestStoriesCacheSeconds": 60, // TTL for the best-stories ID list "StoryDetailsCacheSeconds": 300 // TTL for individual story details }, "OpenTelemetry": { "ServiceName": "hacker-news-api", "ServiceVersion": "1.0.0", "OtlpEndpoint": "http://localhost:4317" } }