Skip to content

renanzitoo/external-api-consumer

Repository files navigation

API Consumer — ZIP Code Lookup

.NET C# MySQL License

An educational ASP.NET Core Web API that demonstrates best practices for consuming external APIs without compromising your application's stability. It integrates with the public ViaCEP API to look up Brazilian ZIP codes, applying resilience techniques, local caching, and complete call auditing.


Table of Contents


About

This project was built to illustrate how to safely integrate a third-party API into a production-ready service. The key goals are:

  • Never crash when the external API is offline or returns unexpected data
  • Never hang when the external API is slow
  • Reduce external calls with intelligent local caching
  • Audit every call for observability and debugging
  • Return consistent, informative errors to consumers

Features

  • ZIP code lookup via the ViaCEP public API
  • Cache-aside — results are stored in MySQL and served from cache on subsequent requests
  • Retry policy — up to 3 retries with exponential backoff (2 s, 4 s, 8 s) via Polly
  • 10-second HTTP timeout to prevent the app from hanging
  • Full audit log of every external call (status code, response time, content)
  • Input validation — rejects anything that isn't an 8-digit ZIP code
  • Standardized envelope — every response uses ApiResponse<T> with a UTC timestamp
  • Swagger / OpenAPI docs available in development

Tech Stack

Layer Technology
Runtime .NET 8.0, ASP.NET Core Web API
ORM Entity Framework Core 8.0 (Pomelo MySQL provider)
Database MySQL 8.0
Resilience Polly 10 (WaitAndRetryAsync, HTTP timeout)
Mapping AutoMapper 12
Docs Swashbuckle / Swagger

Architecture

Request Flow

Client
  │
  ▼
CepController          (validates input, maps exceptions → HTTP status)
  │
  ▼
CepService
  ├─► MySQL (cache hit?)  ──yes──► return cached result
  │
  └─► ViaCEP API  ──────────────► store in MySQL + audit log ──► return result
          │
          └── on failure ────────► audit log ──► throw (503)

Design Patterns

Pattern Where
Cache-Aside CepService checks MySQL before calling ViaCEP
Audit Log Every external call is recorded in ExternalApiCalls
DTO CepResponseDTO, ViaCepResponseDTO keep entities internal
Response Envelope ApiResponse<T> wraps every response with success, data, error, timestamp
Dependency Injection ICepService injected into the controller

Resilience Configuration

// HttpClient with 10-second timeout + 3 retries (exponential backoff)
builder.Services.AddHttpClient<ICepService, CepService>(client =>
{
    client.Timeout = TimeSpan.FromSeconds(10);
}).AddTransientHttpErrorPolicy(policy =>
    policy.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)))
);

Retry wait times: 2 s → 4 s → 8 s (total worst-case: ~24 s before giving up).


Project Structure

ApiConsumer/
├── Controllers/
│   └── CepController.cs          # GET /api/cep/{cep}
├── Services/
│   └── CepService.cs             # Business logic, caching, ViaCEP call
├── Interfaces/
│   └── ICepService.cs            # Service contract
├── Context/
│   ├── AppDbContext.cs           # EF Core DbContext
│   └── AppDbContextFactory.cs   # Design-time factory for migrations
├── Entities/
│   ├── Cep.cs                    # Cached ZIP code record
│   └── ExternalApiCall.cs        # Audit log entry
├── DTOs/
│   ├── Responses/
│   │   ├── ApiResponse.cs        # Generic response envelope
│   │   ├── ApiErrorResponse.cs   # Error payload
│   │   └── CepResponseDTO.cs     # Public address DTO
│   └── External/
│       └── ViaCepResponseDTO.cs  # ViaCEP API deserialization DTO
├── Mappings/
│   └── CepProfile.cs             # AutoMapper: Cep → CepResponseDTO
└── Migrations/                   # EF Core migrations

Getting Started

Prerequisites

  • .NET SDK 8
  • MySQL 8.0 (local install or Docker)
  • Git

1. Clone the repo

git clone https://github.com/renanzitoo/ApiConsumer.git
cd ApiConsumer

2. Configure the database

Option A — local MySQL: edit ApiConsumer/appsettings.json:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Port=3306;Database=apiconsumer;User=root;Password=your-password;"
  }
}

Option B — Docker:

docker run --name mysql-apiconsumer \
  -e MYSQL_ROOT_PASSWORD=root \
  -e MYSQL_DATABASE=apiconsumer \
  -p 3306:3306 -d mysql:8.0

3. Apply migrations

dotnet ef database update --project ApiConsumer

4. Run

dotnet run --project ApiConsumer

The API will be available at https://localhost:5001 (or http://localhost:5000).
Swagger UI: https://localhost:5001/swagger


API Reference

GET /api/cep/{cep}

Looks up a ZIP code. Accepts the code with or without a hyphen (01001000 or 01001-000).

Responses

Status Code Meaning
200 OK ZIP code found (possibly from cache)
400 Bad Request INVALID_CEP Missing or non-8-digit input
404 Not Found CEP_NOT_FOUND ZIP code does not exist in ViaCEP
503 Service Unavailable CEP_SERVICE_UNAVAILABLE ViaCEP unreachable after retries

200 — Success

GET /api/cep/01001000
{
  "success": true,
  "data": {
    "cepCode": "01001000",
    "street": "Praça da Sé",
    "neighborhood": "",
    "city": "São Paulo",
    "state": "SP",
    "uf": "SP"
  },
  "error": null,
  "timestamp": "2026-03-16T12:00:00.000Z"
}

400 — Invalid input

GET /api/cep/123
{
  "success": false,
  "data": null,
  "error": {
    "code": "INVALID_CEP",
    "message": "CEP inválido. Informe 8 dígitos."
  },
  "timestamp": "2026-03-16T12:00:00.000Z"
}

404 — Not found

GET /api/cep/99999999
{
  "success": false,
  "data": null,
  "error": {
    "code": "CEP_NOT_FOUND",
    "message": "CEP não encontrado."
  },
  "timestamp": "2026-03-16T12:00:00.000Z"
}

503 — Upstream unavailable

{
  "success": false,
  "data": null,
  "error": {
    "code": "CEP_SERVICE_UNAVAILABLE",
    "message": "Serviço de CEP indisponível no momento."
  },
  "timestamp": "2026-03-16T12:00:00.000Z"
}

Database Schema

Ceps

Column Type Notes
Id CHAR(36) Primary key (GUID)
CepCode VARCHAR(8) Normalized (digits only)
Street VARCHAR(200)
Neighborhood VARCHAR(100)
City VARCHAR(100)
State VARCHAR(100)
UF VARCHAR(2) State abbreviation
CreatedAt DATETIME
UpdatedAt DATETIME

ExternalApiCalls

Column Type Notes
Id CHAR(36) Primary key (GUID)
Provider VARCHAR(50) e.g. ViaCEP
Endpoint VARCHAR(500) Full URL called
RequestKey VARCHAR(100) ZIP code used
Success BOOLEAN
ResponseStatusCode INT HTTP status
ResponseContent TEXT Raw response body
ResponseTimeInMilliseconds INT
RequestedAt DATETIME

Testing

Swagger UI

Open https://localhost:5001/swagger in a browser.

curl

curl -X GET "https://localhost:5001/api/cep/01001000" \
     -H "Accept: application/json"

PowerShell

Invoke-RestMethod -Uri "https://localhost:5001/api/cep/01001000" -Method Get

JavaScript (fetch)

const res = await fetch('https://localhost:5001/api/cep/01001000');
const data = await res.json();
console.log(data);

Concepts Practiced

  • External API consumption with typed HttpClient
  • Resilience: timeout + retry with exponential backoff (Polly)
  • Cache-Aside pattern to reduce upstream dependency
  • Audit logging that never breaks the main flow
  • async/await throughout
  • Entity Framework Core with MySQL (Pomelo provider)
  • AutoMapper for clean DTO separation
  • Input validation and domain exceptions
  • Standardized API response envelope

Roadmap

  • Structured logging with ILogger / Serilog
  • Circuit Breaker with Polly
  • Unit tests (xUnit + Moq)
  • Integration tests (WebApplicationFactory)
  • Health checks (/healthz)
  • Rate limiting
  • Prometheus metrics
  • Docker Compose setup
  • CI/CD with GitHub Actions

Author

Renan Costagithub.com/renanzitoo


If this project was useful, consider giving it a ⭐

About

ASP.NET Core API demonstrating resilient external API consumption using caching, auditing, retries, and standardized error handling.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages