Most tutorials show you how to build an API. Fewer show you what sits in front of the API in a real production system.
This project is about that middle layer. A gateway that all requests go through before they reach the actual API. It checks authentication, enforces rate limits, and routes traffic. In Azure this is what API Management does. Here it runs locally with YARP, Microsoft's open source reverse proxy library.
The actual application is a travel booking system. The gateway pattern is the interesting part.
Why a gateway matters
When you build an API and expose it directly to clients, every client knows your API's address. They can call it as many times as they want. There is no central place to enforce authentication or throttle abusive callers.
A gateway changes that. Clients only know the gateway address. The API address is internal. The gateway handles auth and rate limiting before requests ever reach your code.
This is how companies like Qatar Airways, booking.com, and most large travel platforms structure their APIs. One gateway, many services behind it.
The architecture
Three .NET projects and one Angular app.
TravelBooking.Api is a standard ASP.NET Core 10 API. It connects to MongoDB, handles CRUD operations for bookings, and sends notifications when bookings are created or confirmed. The notification goes to a Logic Apps webhook in production. Locally it logs to the console.
TravelBooking.Gateway is the interesting one. Built with YARP. It does three things on every request: checks the X-Api-Key header, enforces rate limits, and proxies to the API with path transformation.
TravelBooking.Core holds shared models and interfaces.
travel-booking-ui is Angular 19. It calls the gateway, never the API directly.
Setting up YARP as a gateway
YARP configuration lives in appsettings.json. You define routes and clusters:
"ReverseProxy": {
"Routes": {
"bookings-route": {
"ClusterId": "bookings-cluster",
"Match": {
"Path": "/gateway/{**catch-all}"
},
"Transforms": [
{ "PathRemovePrefix": "/gateway" }
]
}
},
"Clusters": {
"bookings-cluster": {
"Destinations": {
"destination1": {
"Address": "http://localhost:5153"
}
}
}
}
}
Any request to /gateway/api/bookings gets the /gateway prefix stripped and forwarded to http://localhost:5153/api/bookings. The client never needs to know the API address.
API key authentication
This middleware runs before YARP proxies the request:
app.Use(async (context, next) =>
{
var apiKey = context.Request.Headers["X-Api-Key"].FirstOrDefault();
if (string.IsNullOrEmpty(apiKey) || apiKey != builder.Configuration["Gateway:ApiKey"])
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized: Invalid or missing API key");
return;
}
await next();
});
No API key, no access. The request never reaches the API.
Rate limiting
10 requests per 10 seconds per client. After that, requests queue up to a limit of 5 then get rejected.
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", opt =>
{
opt.PermitLimit = 10;
opt.Window = TimeSpan.FromSeconds(10);
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
opt.QueueLimit = 5;
});
});
In production you would use sliding window or token bucket depending on your traffic patterns. Fixed window is the simplest to understand and works fine for most cases.
MongoDB for bookings
The repository pattern keeps the MongoDB driver out of the controllers:
public async Task<Booking> CreateAsync(Booking booking)
{
await _bookings.InsertOneAsync(booking);
_logger.LogInformation("Booking {BookingId} created for {Customer}",
booking.Id, booking.CustomerName);
return booking;
}
MongoDB is a good fit for bookings. The document structure matches naturally, and you do not need to define a schema before you start. Add a new field to the model and it just works.
The Angular frontend
The booking service sets the API key on every request automatically:
private headers = new HttpHeaders({
'Content-Type': 'application/json',
'X-Api-Key': this.apiKey
});
getBookings(): Observable<Booking[]> {
return this.http.get<Booking[]>(
`${this.gatewayUrl}/api/bookings`,
{ headers: this.headers }
);
}
The gateway URL is the only thing Angular knows about. It has no idea where the API runs.
What a real request looks like
A booking creation goes through this path:
- Angular sends POST to http://localhost:5177/gateway/api/bookings with X-Api-Key header
- Gateway middleware validates the API key
- Rate limiter checks the request count
- YARP strips /gateway and forwards to http://localhost:5153/api/bookings
- API saves to MongoDB and logs the notification
- Response comes back through the gateway to Angular
The whole thing takes under 200ms locally.
Running it yourself
You need .NET 10, Node.js 20, and Docker Desktop.
git clone https://github.com/aftabkh4n/travel-booking-platform.git
cd travel-booking-platform
docker run -d --name travel-mongo -p 27017:27017 mongo:7
cp TravelBooking.Api/appsettings.example.json TravelBooking.Api/appsettings.json
cp TravelBooking.Gateway/appsettings.example.json TravelBooking.Gateway/appsettings.json
Start the API, then the gateway, then Angular. Open http://localhost:4200.
Source code: https://github.com/aftabkh4n/travel-booking-platform
If you have questions drop them in the comments.
Top comments (0)