· 10 min read
Part 5: Final API Polish - CORS, Health Checks, Versioning, and React Integration Prep
Okay, let’s proceed with Part 5, focusing on the final API touches and preparing for our React frontend integration.
Astrowind Blog Post: Building a Modern Web App with ASP.NET Core & React
Part 5: Final API Polish - CORS, Health Checks, Versioning, and React Integration Prep
In Part 4, we implemented PDF file handling, pagination, and global error management, significantly enhancing our eLibrary API. In this final API-focused part, we’ll address Cross-Origin Resource Sharing (CORS) to allow our React frontend to communicate with the API, briefly touch upon API versioning, set up a health check endpoint, and do a quick review before we start building the React application.
5.1. CORS (Cross-Origin Resource Sharing)
When your React frontend (running on, say, http://localhost:3000
) tries to make requests to your ASP.NET Core API (running on, say, https://localhost:7001
), the browser’s Same-Origin Policy will block these requests by default because they originate from different “origins” (protocol, domain, or port). CORS is a mechanism that allows servers to specify which origins are permitted to access its resources.
5.1.1. Configure CORS in Program.cs
We’ll define a CORS policy that allows requests from our development React server and potentially our production frontend domain.
Modify Program.cs
:
// eLibrary.API/Program.cs
// ... (other usings)
var builder = WebApplication.CreateBuilder(args);
// Define a specific CORS policy name
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
policy =>
{
// For development, allow your React app's origin
// For production, replace with your actual frontend domain(s)
var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>()
?? new[] { "http://localhost:3000" }; // Default if not in config
policy.WithOrigins(allowedOrigins) // Specify allowed origins
.AllowAnyHeader() // Allow any HTTP headers
.AllowAnyMethod() // Allow any HTTP methods (GET, POST, PUT, DELETE, etc.)
.AllowCredentials(); // If your frontend needs to send cookies or auth headers with CORS
// Output (policy configured): (No direct output, but policy is registered)
// When a request comes, if its Origin header matches one in WithOrigins,
// and method/headers are allowed, CORS headers are added to the response.
});
});
// ... (other service registrations: AddDbContext, AddIdentity, AddAuthentication, AddAutoMapper, Repositories, Services, AddControllers)
var app = builder.Build();
// ... (ExceptionMiddleware, Swagger)
app.UseHttpsRedirection(); // Important: Keep this before UseCors if you enforce HTTPS
// IMPORTANT: Add UseCors middleware. It should generally be placed
// before UseAuthentication and UseAuthorization.
// The order is: Exception Handling -> Routing -> CORS -> AuthN -> AuthZ -> Endpoints
app.UseRouting(); // Establishes route before CORS policy is checked for that route.
app.UseCors(MyAllowSpecificOrigins); // Apply the CORS policy
// Output (runtime): If an incoming request has an Origin header matching the policy,
// appropriate CORS response headers (e.g., Access-Control-Allow-Origin) are added.
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
5.1.2. Add CORS Configuration to appsettings.json
It’s good practice to make allowed origins configurable.
Open appsettings.json
(or appsettings.Development.json
for dev-specific settings):
{
// ... (Logging, ConnectionStrings, JwtSettings, AppSettings)
"Cors": {
"AllowedOrigins": [
"http://localhost:3000", // Your React dev server
"https://your-production-frontend-domain.com" // Example production domain
]
// Output (when read): The array of allowed origin strings.
}
}
If Cors:AllowedOrigins
is not found in the configuration, it defaults to http://localhost:3000
.
How to Test CORS ( conceptually, as React isn’t built yet):
- Once your React app is running on
http://localhost:3000
. - Make an API call from React (e.g.,
fetch('https://localhost:7001/api/books')
). - Open your browser’s developer console.
- Without CORS configured properly: You’d see an error like “Access to fetch at ‘https://localhost:7001/api/books’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.”
- With CORS configured correctly: The request should succeed (assuming the API endpoint works and authentication, if any, is handled). The response headers from the API will include
Access-Control-Allow-Origin: http://localhost:3000
.
Important Note on AllowCredentials()
: If your frontend needs to send credentials (like cookies for session-based auth, or if you use JWTs in HttpOnly cookies which are sent automatically), you must use AllowCredentials()
. When AllowCredentials()
is used, WithOrigins()
cannot be set to *
(wildcard); you must specify explicit origins. Also, the frontend (e.g., fetch
options) must also indicate it’s sending credentials.
5.2. API Health Checks
Health check endpoints provide a simple way for monitoring tools or orchestrators (like Kubernetes) to verify if your API is running and healthy.
5.2.1. Install Health Checks NuGet Package (if not already included) Most ASP.NET Core Web API projects include this by default, but if not:
Install-Package Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore
# Output: (NuGet installation messages)
// This one specifically allows checking DbContext health.
// For basic health checks, Microsoft.AspNetCore.Diagnostics.HealthChecks might be sufficient.
5.2.2. Configure Health Checks in Program.cs
// eLibrary.API/Program.cs
// ... (other usings)
using eLibrary.API.Data; // For ApplicationDbContext
using Microsoft.Extensions.Diagnostics.HealthChecks; // For HealthCheckResult
var builder = WebApplication.CreateBuilder(args);
// ... (CORS, other service registrations)
// 7. Add Health Checks
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy("API is running."), tags: new[] { "live" }) // Basic liveness check
// Output (when /health/live is hit): {"status":"Healthy","description":"API is running."}
.AddDbContextCheck<ApplicationDbContext>(name: "database", failureStatus: HealthStatus.Unhealthy, tags: new[] { "ready" }); // DB readiness check
// Output (when /health/ready is hit, if DB is ok): {"status":"Healthy","description":"ApplicationDbContext is healthy."}
// Output (if DB is down): {"status":"Unhealthy","description":"ApplicationDbContext is unhealthy."}
var app = builder.Build();
// ... (Middleware pipeline)
// Map Health Check Endpoints
// It's common to have separate liveness and readiness probes
app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
{
Predicate = (check) => check.Tags.Contains("live"), // Only run checks tagged 'live'
ResponseWriter = async (context, report) => // Custom response writer (optional)
{
var result = JsonSerializer.Serialize(new
{
status = report.Status.ToString(),
checks = report.Entries.Select(e => new { name = e.Key, status = e.Value.Status.ToString(), description = e.Value.Description, duration = e.Value.Duration.ToString() }),
totalDuration = report.TotalDuration.ToString()
});
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(result);
}
});
// Output (when hitting /health/live):
// {
// "status": "Healthy",
// "checks": [ { "name": "self", "status": "Healthy", "description": "API is running.", "duration": "00:00:00.0010000" } ],
// "totalDuration": "00:00:00.0020000"
// }
app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
{
Predicate = (check) => check.Tags.Contains("ready"), // Only run checks tagged 'ready'
ResponseWriter = async (context, report) => // Using the same custom writer
{
var result = JsonSerializer.Serialize(new
{
status = report.Status.ToString(),
checks = report.Entries.Select(e => new { name = e.Key, status = e.Value.Status.ToString(), description = e.Value.Description, duration = e.Value.Duration.ToString() }),
totalDuration = report.TotalDuration.ToString()
});
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(result);
}
});
// Output (when hitting /health/ready, assuming DB is fine):
// {
// "status": "Healthy",
// "checks": [ { "name": "database", "status": "Healthy", "description": "ApplicationDbContext is healthy.", "duration": "00:00:00.0500000" } ],
// "totalDuration": "00:00:00.0510000"
// }
// A general health check endpoint (optional)
// app.MapHealthChecks("/health");
app.MapControllers();
app.Run();
Explanation:
AddHealthChecks()
: Registers the health check services.AddCheck("self", ...)
: A simple custom health check for basic API liveness.AddDbContextCheck<ApplicationDbContext>()
: Checks if the database connection is healthy.MapHealthChecks("/health/live", ...)
: Exposes an endpoint at/health/live
.Predicate
: Filters which checks run for this endpoint (useful for distinguishing liveness vs. readiness).ResponseWriter
: Customizes the JSON output (optional, default is simpler).
- Liveness vs. Readiness:
- Liveness (
/health/live
): Indicates if the application process is running. If this fails, the orchestrator might restart the instance. Should be quick and check internal state. - Readiness (
/health/ready
): Indicates if the application is ready to accept traffic (e.g., database connected, initial caches loaded). If this fails, the orchestrator won’t send traffic to this instance but might not restart it immediately.
- Liveness (
Testing Health Checks:
- Run your API.
- Navigate to
/health/live
in your browser. You should see a JSON response indicating “Healthy”. - Navigate to
/health/ready
. You should see a JSON response indicating “Healthy” (assuming your DB is accessible). - (Optional Test) Stop your SQL Server LocalDB instance and refresh
/health/ready
. It should now report “Unhealthy” for the database check.
5.3. API Versioning (Brief Overview)
As your API evolves, you might introduce breaking changes. API versioning allows you to manage different versions of your API concurrently, giving clients time to adapt.
Common Strategies:
- URL Path Versioning:
https://api.example.com/v1/books
,https://api.example.com/v2/books
- Query String Versioning:
https://api.example.com/books?api-version=1.0
- Header Versioning: Custom header like
X-API-Version: 1.0
- Media Type Versioning (Content Negotiation):
Accept: application/vnd.myapi.v1+json
ASP.NET Core offers packages for easy versioning implementation (e.g., Microsoft.AspNetCore.Mvc.Versioning
and Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
for Swagger integration).
Example: URL Path Versioning Setup (Conceptual)
Install Packages:
Install-Package Microsoft.AspNetCore.Mvc.Versioning Install-Package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
Configure in
Program.cs
:// builder.Services.AddControllers(); // Existing builder.Services.AddApiVersioning(options => { options.DefaultApiVersion = new Microsoft.AspNetCore.Mvc.ApiVersion(1, 0); options.AssumeDefaultVersionWhenUnspecified = true; options.ReportApiVersions = true; // Adds api-supported-versions and api-deprecated-versions headers // options.ApiVersionReader = new UrlSegmentApiVersionReader(); // Default }); builder.Services.AddVersionedApiExplorer(options => { options.GroupNameFormat = "'v'VVV"; // Formats the version in Swagger e.g. v1, v2 options.SubstituteApiVersionInUrl = true; }); // In Swagger Gen configuration (within AddSwaggerGen): // services.AddSwaggerGen(c => { // var provider = services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>(); // foreach (var description in provider.ApiVersionDescriptions) // { // c.SwaggerDoc(description.GroupName, new OpenApiInfo { Title = $"eLibrary API {description.ApiVersion}", Version = description.ApiVersion.ToString() }); // } // });
Apply to Controllers:
[ApiVersion("1.0")] [Route("api/v{version:apiVersion}/[controller]")] public class BooksControllerV1 : ControllerBase { /* ... V1 logic ... */ } [ApiVersion("2.0")] [Route("api/v{version:apiVersion}/[controller]")] public class BooksControllerV2 : ControllerBase { /* ... V2 logic ... */ }
This is a simplified overview. Full implementation requires careful planning of how versions affect DTOs, services, and Swagger documentation. For our eLibrary, we might not implement full versioning from the start but it’s good to be aware of.
5.4. Final Review and Conventions
Let’s quickly recap some conventions and best practices we’ve aimed for:
- Project Structure: Clear separation of concerns (Models, DTOs, Repositories, Services, Controllers, Helpers, Mappings, Middleware).
- Naming Conventions:
- PascalCase for classes, methods, properties (
BookService
,GetBookByIdAsync
). I
prefix for interfaces (IBookService
).Async
suffix for asynchronous methods.- DTOs often suffixed with
Dto
(BookDto
,CreateBookDto
).
- PascalCase for classes, methods, properties (
- Async/Await: Used throughout for I/O-bound operations to maintain responsiveness.
- Dependency Injection: Heavily used for loose coupling and testability.
- Repository Pattern: Abstracting data access.
- Service Layer: Encapsulating business logic.
- DTOs: For API contracts, decoupling from entities.
- AutoMapper: For object-to-object mapping.
- ASP.NET Core Identity & JWT: For robust authentication and authorization.
- Error Handling: Global exception middleware for unhandled exceptions; specific
NotFound
,BadRequest
,Unauthorized
,Forbid
results in controllers. - Logging: Using
ILogger
throughout for diagnostics. - Configuration: Using
appsettings.json
and User Secrets for sensitive data. - RESTful Principles:
- Using HTTP verbs correctly (GET, POST, PUT, DELETE).
- Meaningful resource URLs (
/api/books
,/api/books/{id}
). - Standard HTTP status codes.
- File Handling: Securely saving and serving files.
- Pagination: For efficient handling of large datasets.
- CORS: To allow cross-origin requests from the frontend.
- Health Checks: For monitoring API status.
5.5. Preparing for React Integration: What the Frontend Needs
Our React frontend will interact with the API by making HTTP requests to the endpoints we’ve built. It will need to:
- Know the API Base URL: e.g.,
https://localhost:7001/api
- Authentication Endpoints:
POST /auth/register
(sendsRegisterDto
, receivesAuthResponseDto
)POST /auth/login
(sendsLoginDto
, receivesAuthResponseDto
)
- Token Management: Store the JWT from
AuthResponseDto.Token
and send it in theAuthorization: Bearer <token>
header for protected requests. Handle token expiration and refresh (though we haven’t built refresh token logic in this series, it’s a common extension). - Book Endpoints:
GET /books
(optionally with query params fromBookParams
likepageNumber
,pageSize
,titleFilter
). Receives a list ofBookDto
in the body and pagination info in thePagination
header.GET /books/{id}
(receivesBookDto
)POST /books
(Admin only, sendsmultipart/form-data
containingCreateBookDto
fields andPdfFile
). ReceivesBookDto
.PUT /books/{id}
(Admin only, sendsmultipart/form-data
containingUpdateBookDto
fields and optionalPdfFile
). Receives HTTP 204.DELETE /books/{id}
(Admin only). Receives HTTP 204.GET /books/{id}/pdf
(Downloads the PDF file).
- DTO Structures: The React app will need to construct objects matching our DTOs for POST/PUT requests and expect data matching our DTOs in responses.
- Error Handling: Interpret HTTP status codes (200, 201, 204, 400, 401, 403, 404, 500) and display appropriate messages to the user. Parse error messages from the response body if provided (e.g., validation errors from a 400 response, or our
ApiExceptionResponse
for 500s).
We now have a solid, well-structured ASP.NET Core backend API ready to serve our eLibrary’s React frontend.
This concludes the ASP.NET Core backend development for our eLibrary project guide! We’ve covered a lot of ground, from initial setup to advanced features.
Next Steps (The React Frontend Series - Astrowind Blog Style):
- Setting up the React project (e.g., using Create React App or Vite).
- Integrating the Astrowind template.
- Structuring the React application (components, services, routing).
- Implementing authentication (login, registration, token handling).
- Building UI components for displaying books, authors, etc.
- Implementing forms for creating/editing books, including file uploads.
- Handling state management (e.g., Context API, Redux, Zustand).
- Making API calls to our .NET backend.
- Displaying PDF files or providing download links.
- Role-based UI rendering.
I’m ready to start outlining the React part whenever you are! We’ll reference the API endpoints and DTOs we’ve just defined.