GB.
.NET Backend - Step by Step
Step 7 of 888% through series
  1. 7
  2. 8
2026-03-0915 min read

API Gateway for Beginners: From Basic Routing to Production-Ready Microservices

#.NET#ASP.NET Core#API Gateway#Ocelot#Microservices#JWT#Rate Limiting#Caching#RabbitMQ#Kafka#Azure Service Bus#Docker#Kubernetes

.NET API Gateway for Beginners: From Basic Routing to Production-Ready Microservices

If you are starting with microservices in .NET, one of the first problems you will encounter is:

"My application has many APIs. How should the frontend communicate with all of them?"

Imagine that your application has these services:

Frontend
   |
   +----> User Service       :5001
   |
   +----> Product Service    :5002
   |
   +----> Order Service      :5003
   |
   +----> Payment Service    :5004
   |
   +----> Notification       :5005

This works, but as the system grows, several problems appear.

The frontend needs to know where every service is located. Authentication may need to be handled by every service. Rate limiting may need to be implemented everywhere. Logging, CORS, security, caching, and other cross-cutting concerns become duplicated.

This is where an API Gateway becomes useful.


1. What Problem Does an API Gateway Solve?

Without a gateway, the frontend communicates directly with every microservice:

                    +--> User Service
                    |
Frontend ---------->+--> Product Service
                    |
                    +--> Order Service
                    |
                    +--> Payment Service

The frontend now needs to know:

  • Where each service is running
  • Which port each service uses
  • How authentication works
  • Which service requires which headers
  • How to handle failures
  • How to handle rate limits
  • How to communicate with different services

This creates unnecessary complexity.

With an API Gateway:

                    +--> User Service
                    |
Frontend --> Gateway +--> Product Service
                    |
                    +--> Order Service
                    |
                    +--> Payment Service

The frontend only knows about the gateway.

For example:

https://api.mycompany.com/users
https://api.mycompany.com/products
https://api.mycompany.com/orders

The gateway decides where those requests should go.

So the gateway becomes the front door of your microservices architecture.


2. What Can an API Gateway Do?

An API Gateway can handle many cross-cutting responsibilities:

  • Request routing
  • Authentication and authorization
  • Rate limiting
  • Response caching
  • CORS
  • IP whitelisting
  • Circuit breakers
  • Health checks
  • Logging
  • Metrics
  • Load balancing
  • Request transformation

It can also sit between your public API and internal services.

The important idea is:

Don't start by adding every feature. Start with routing and gradually add the features your system actually needs.


3. Our Example Architecture

Throughout this article, let's imagine we have:

                 Frontend
                    |
                    v
             +--------------+
             | API Gateway  |
             |   :5000      |
             +--------------+
               /     |     \
              /      |      \
             v       v       v
         Product    Order    User
         :5001      :5002    :5003

We'll use ASP.NET Core + Ocelot for the gateway.


4. Creating the Gateway

Create a Web API project:

dotnet new webapi -n MyGateway
cd MyGateway

Install Ocelot:

dotnet add package Ocelot

If you want Ocelot caching support:

dotnet add package Ocelot.Cache.CacheManager

Ocelot provides the routing/middleware functionality required to build our gateway.


5. Basic Routing

Create an ocelot.json file.

For example:

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/api/products",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5001
        }
      ],
      "UpstreamPathTemplate": "/products",
      "UpstreamHttpMethod": ["GET", "POST"]
    },
    {
      "DownstreamPathTemplate": "/api/orders",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 5002
        }
      ],
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": ["GET"]
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5000"
  }
}

The important concept here is upstream vs downstream.

Upstream

The URL exposed by the gateway:

/products

Downstream

The actual microservice endpoint:

http://localhost:5001/api/products

Therefore:

GET http://localhost:5000/products
             |
             v
          Gateway
             |
             v
GET http://localhost:5001/api/products

The frontend doesn't need to know that Product Service is running on port 5001.


6. Basic Program.cs

The initial Program.cs can be very simple:

using Ocelot.DependencyInjection;
using Ocelot.Middleware;

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddJsonFile("ocelot.json");

builder.Services.AddOcelot();

var app = builder.Build();

await app.UseOcelot();

app.Run();

That's enough to get basic routing working.

At this point:

/products --> Product Service
/orders   --> Order Service

Your gateway is alive.

But a production gateway needs much more.


7. Environment Configuration

We normally don't want to hard-code production URLs into ocelot.json.

Development might use:

localhost:5001
localhost:5002

Production might use:

product-service
order-service

or Azure/container DNS names.

ASP.NET Core supports environment-specific configuration.

For example:

appsettings.json
appsettings.Development.json
appsettings.Production.json

ocelot.json
ocelot.Development.json
ocelot.Production.json

You can load them like this:

var builder = WebApplication.CreateBuilder(args);

builder.Configuration
    .AddJsonFile("appsettings.json", optional: false)
    .AddJsonFile(
        $"appsettings.{builder.Environment.EnvironmentName}.json",
        optional: true)
    .AddJsonFile("ocelot.json")
    .AddJsonFile(
        $"ocelot.{builder.Environment.EnvironmentName}.json",
        optional: true)
    .AddEnvironmentVariables();

Why AddEnvironmentVariables()?

It allows configuration values to come from environment variables.

For example:

Jwt__Issuer
Jwt__Audience
Jwt__Key
Redis__Connection

can override configuration values.

This is particularly useful in Docker, Kubernetes, and Azure because you don't need to put secrets directly into your source code.

A good rule is:

Configuration can live in files. Secrets should normally come from a secure configuration mechanism or environment/infrastructure configuration.


8. Rate Limiting

Now imagine someone starts sending:

10,000 requests/second

to your API.

Your gateway can protect downstream services using rate limiting.

For example:

{
  "RateLimitOptions": {
    "EnableRateLimiting": true,
    "Period": "1m",
    "Limit": 60,
    "ClientWhitelist": ["admin"],
    "QuotaExceededResponse": {
      "StatusCode": 429,
      "Content": "{\"error\":\"Rate limit exceeded\"}"
    }
  }
}

This means a client may only make a certain number of requests within a time period.

When the limit is exceeded:

HTTP 429 Too Many Requests

The gateway stops excessive traffic before it reaches your microservices.

This protects:

Gateway
   |
   X ---- Too many requests
   |
Microservices

instead of allowing the traffic to overload the backend.


9. Caching

Suppose your product catalog changes only every few minutes.

Why call Product Service every time?

Client 1 --> Product Service
Client 2 --> Product Service
Client 3 --> Product Service
Client 4 --> Product Service

Instead, the gateway can cache responses.

For example:

{
  "FileCacheOptions": {
    "TtlSeconds": 60,
    "Region": "product-catalog"
  }
}

Now:

First request
Client --> Gateway --> Product Service
                    |
                    v
                  Cache

Next request
Client --> Gateway --> Cache

The backend doesn't need to process every identical request.

For a single gateway instance, local caching may be enough.

But when you have multiple gateway instances:

             Load Balancer
              /    |    \
             v     v     v
          Gateway Gateway Gateway
             \      |      /
              \     |     /
                 Redis

a distributed cache such as Redis becomes much more useful.


10. JWT Authentication

Security is another major responsibility of the gateway.

Suppose a client sends:

Authorization: Bearer eyJhbGciOi...

The gateway can validate the JWT before forwarding the request.

Basic configuration:

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters =
            new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,

                ValidIssuer =
                    builder.Configuration["Jwt:Issuer"],

                ValidAudience =
                    builder.Configuration["Jwt:Audience"],

                IssuerSigningKey =
                    new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(
                            builder.Configuration["Jwt:Key"]))
            };
    });

builder.Services.AddAuthorization();

Then:

app.UseAuthentication();
app.UseAuthorization();

The idea becomes:

Client
  |
  | JWT
  v
Gateway
  |
  | valid?
  |
  +---- No ----> 401
  |
  +---- Yes ---> Microservice

This prevents unauthorized requests from reaching protected services.


11. Health Checks

Your gateway should also expose a simple health endpoint.

For example:

app.MapGet("/health", () =>
    Results.Ok(new
    {
        status = "healthy"
    }))
    .AllowAnonymous();

Now:

GET /health

returns:

{
  "status": "healthy"
}

Why is this useful?

Load balancers, Kubernetes, Azure, monitoring systems, and deployment systems can use health endpoints to determine whether your application is alive.

You can also create detailed health checks for dependencies such as databases, Redis, or other services.


12. IP Whitelisting

Sometimes an endpoint should only be accessible from specific networks or IP addresses.

For example:

Allowed:
10.0.0.10
10.0.0.20

Everything else:
403 Forbidden

A simple middleware example:

app.Use(async (context, next) =>
{
    var allowedIPs = new[]
    {
        "192.168.1.1",
        "10.0.0.1"
    };

    var clientIP =
        context.Connection.RemoteIpAddress?.ToString();

    if (!allowedIPs.Contains(clientIP))
    {
        context.Response.StatusCode = 403;
        await context.Response.WriteAsync("Access denied");
        return;
    }

    await next();
});

However, in production, be careful with proxies and load balancers because the IP visible to your application may be the proxy's IP rather than the original client's IP.


13. Circuit Breaker

Now imagine your Order Service is down.

Without protection:

Client
  |
Gateway
  |
Order Service
  X
Failure

The gateway keeps sending requests.

Eventually:

100 requests
100 failures
1000 requests
1000 failures

A circuit breaker helps stop repeatedly calling a failing service.

Conceptually:

             Healthy
                |
             failures
                v
              OPEN
                |
        stop calling service
                |
             timeout
                v
           Half Open
                |
       test request
          /          \
       success      failure
          |            |
          v            v
       CLOSED        OPEN

With Ocelot QoS configuration:

{
  "QoSOptions": {
    "ExceptionsAllowedBeforeBreaking": 3,
    "DurationOfBreak": 60,
    "TimeoutValue": 5000
  }
}

Here the gateway can stop calling a service after repeated failures and give the service time to recover.

This is one of the most important resilience concepts in microservices.


14. CORS

If your frontend runs at:

https://myfrontend.com

and your gateway runs at:

https://api.mycompany.com

the browser considers them different origins.

You can configure CORS:

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend", policy =>
    {
        policy
            .WithOrigins(
                "https://myfrontend.com",
                "https://localhost:3000")
            .AllowAnyMethod()
            .AllowAnyHeader();
    });
});

Then:

app.UseCors("AllowFrontend");

Avoid this in production unless you have a specific reason:

.AllowAnyOrigin()

Instead, explicitly specify the origins that should be allowed.


15. Synchronous vs Asynchronous Communication

Until now, we've mostly talked about synchronous communication.

For example:

Frontend
   |
Gateway
   |
Order Service
   |
Response

The client waits for the response.

But sometimes services don't need to wait for each other.

For example:

Order Created
     |
     +----> Send Email
     |
     +----> Update Analytics
     |
     +----> Notify Warehouse

Instead of calling every service synchronously, we can publish an event:

Order Service
      |
      v
 Message Broker
      |
      +----> Notification Service
      |
      +----> Analytics Service
      |
      +----> Warehouse Service

This is asynchronous messaging.


16. RabbitMQ

RabbitMQ is commonly used for message-based communication.

Conceptually:

Producer
   |
   v
RabbitMQ
   |
   v
Consumer

A simple producer might look like:

var factory = new ConnectionFactory
{
    HostName = "localhost"
};

using var connection = factory.CreateConnection();
using var channel = connection.CreateModel();

channel.QueueDeclare(
    "orders",
    durable: true);

var body =
    Encoding.UTF8.GetBytes(message);

channel.BasicPublish(
    "",
    "orders",
    null,
    body);

RabbitMQ is a good option when your application needs traditional queues, consumers, acknowledgements, and routing patterns.


17. Kafka

Kafka is another popular messaging platform.

The mental model is slightly different.

Instead of thinking primarily about a queue, think about an event stream/topic.

Producer
   |
   v
Kafka Topic
   |
   +---- Consumer A
   |
   +---- Consumer B
   |
   +---- Consumer C

A .NET producer can use:

dotnet add package Confluent.Kafka

Then:

var config = new ProducerConfig
{
    BootstrapServers = "localhost:9092"
};

using var producer =
    new ProducerBuilder<Null, string>(config)
        .Build();

await producer.ProduceAsync(
    topic,
    new Message<Null, string>
    {
        Value = message
    });

Kafka is particularly useful when you need high-throughput event streaming and multiple consumers need to process the same events independently.


18. Azure Service Bus

If you're running primarily in Azure, Azure Service Bus is another option.

For example:

dotnet add package Azure.Messaging.ServiceBus

A simplified sender:

await using var client =
    new ServiceBusClient(connectionString);

var sender =
    client.CreateSender("orders");

var message =
    new ServiceBusMessage("Order created");

await sender.SendMessageAsync(message);

The important point is that RabbitMQ, Kafka, and Azure Service Bus solve related but different messaging requirements.

Don't choose a technology simply because it is popular.

Choose based on:

  • Message ordering
  • Throughput
  • Delivery guarantees
  • Replay requirements
  • Consumer patterns
  • Cloud environment
  • Operational complexity

19. Logging

Once your system has multiple services, debugging becomes difficult.

Imagine:

Frontend
   |
Gateway
   |
Order Service
   |
Payment Service
   |
Database

A single user request can travel through several applications.

Centralized logging becomes extremely important.

A simple Serilog configuration:

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .WriteTo.File(
        "logs/gateway-.txt",
        rollingInterval: RollingInterval.Day)
    .CreateLogger();

builder.Host.UseSerilog();

In production, you may send logs to centralized systems such as:

Application
    |
    v
Centralized Logging
    |
    +--> Search
    +--> Alerts
    +--> Dashboards

The most useful information to log includes:

  • Request ID / correlation ID
  • HTTP method
  • URL
  • Status code
  • Duration
  • Exception
  • Service name

Avoid logging passwords, tokens, and other sensitive information.


20. Metrics

Logs tell you what happened.

Metrics help you understand how your system is behaving.

Useful gateway metrics include:

Requests per second
Average response time
95th percentile latency
5xx errors
4xx errors
Rate-limit violations
Circuit-breaker failures
CPU
Memory

For example, Prometheus can expose:

/metrics

and monitoring tools can collect those metrics.

A custom counter might look like:

var requestCounter =
    Metrics.CreateCounter(
        "gateway_requests_total",
        "Total gateway requests",
        new CounterConfiguration
        {
            LabelNames = new[]
            {
                "method",
                "path"
            }
        });

Together:

Logs   -> detailed events
Metrics -> system health/performance
Traces -> request journey across services

For larger microservice environments, distributed tracing becomes especially valuable.


21. Deployment

Once the gateway works locally, the next question is:

Where should we run it?

One common option is Docker.

A simplified Dockerfile:

FROM mcr.microsoft.com/dotnet/aspnet:8.0

WORKDIR /app

COPY . .

ENTRYPOINT ["dotnet", "MyGateway.dll"]

Build:

docker build -t my-gateway .

Run:

docker run -d -p 8080:80 my-gateway

Now your gateway runs inside a container.


22. Scaling the Gateway

Suppose one gateway instance receives:

10,000 requests/minute

Eventually you may need multiple instances:

                 Load Balancer
                /      |      \
               v       v       v
          Gateway   Gateway   Gateway

This is called horizontal scaling.

Instead of making one server bigger:

1 huge server

we run:

3 smaller servers

or:

10 instances

depending on traffic.

For this to work well, the gateway should preferably be stateless.

Avoid storing important user/session state only in memory.

For shared caching, use something like Redis.


23. Kubernetes Scaling

Kubernetes can automatically scale gateway instances.

For example:

spec:
  replicas: 3

starts three gateway pods.

You can also configure a Horizontal Pod Autoscaler:

minReplicas: 2
maxReplicas: 10

For example:

Low traffic
    |
    v
2 Gateway Pods

High traffic
    |
    v
5 Gateway Pods

Very high traffic
    |
    v
10 Gateway Pods

The infrastructure automatically adjusts the number of instances based on configured metrics.


24. Putting Everything Together

A production-style architecture might look like this:

                       Internet
                          |
                          v
                 +----------------+
                 | Load Balancer  |
                 +----------------+
                          |
                          v
               +--------------------+
               |    API Gateway     |
               |--------------------|
               | Routing            |
               | JWT                |
               | Rate Limiting      |
               | Caching            |
               | CORS               |
               | IP Filtering       |
               | Circuit Breaker    |
               | Health Checks      |
               | Logging            |
               | Metrics            |
               +--------------------+
                  /      |       \
                 /       |        \
                v        v         v
          User Service Product    Order
                         Service   Service
                                     |
                                     v
                              Message Broker
                              /     |      \
                             v      v       v
                        Notification Analytics
                              Service

This is the bigger picture.


25. What Should You Implement First?

If you are a beginner, don't try to implement everything on day one.

A good progression is:

Step 1 - Basic Routing

Gateway
   |
   +--> Product
   +--> Order

Step 2 - Environment Configuration

Move service URLs and configuration out of hard-coded values.

Step 3 - Authentication

Add JWT validation.

Step 4 - CORS

Allow only your legitimate frontend applications.

Step 5 - Rate Limiting

Protect APIs from excessive traffic.

Step 6 - Caching

Cache suitable GET requests.

Step 7 - Health Checks

Allow infrastructure to determine whether services are healthy.

Step 8 - Circuit Breakers

Prevent cascading failures.

Step 9 - Logging and Metrics

Understand what your system is doing.

Step 10 - Messaging

Introduce RabbitMQ, Kafka, or Azure Service Bus when asynchronous communication is actually needed.

Step 11 - Deployment

Containerize the gateway.

Step 12 - Scaling

Use multiple instances and, when appropriate, Kubernetes or cloud autoscaling.


26. Final Mental Model

If you remember only one thing from this article, remember this:

                    API Gateway
                         |
       +-----------------+----------------+
       |                 |                |
    Security         Performance       Reliability
       |                 |                |
      JWT          Rate Limiting       Circuit Breaker
      CORS              Cache           Health Check
      IP Filter                         Retry/Timeout
       |
       +---------------------------------------+
                                               |
                                               v
                                         Microservices

The API Gateway is not simply a proxy.

It becomes the controlled entry point into your microservices environment.

It can simplify the frontend, centralize common concerns, protect backend services, and provide a consistent place for routing, security, resilience, and observability.

But there is an important lesson:

Don't turn the gateway into another giant application.

Keep business logic inside the appropriate microservices.

The gateway should primarily handle concerns that belong at the edge of your system.

Start simple:

Routing
   ↓
Security
   ↓
Rate Limiting
   ↓
Caching
   ↓
Resilience
   ↓
Observability
   ↓
Messaging
   ↓
Deployment & Scaling

Once you understand this progression, concepts like Ocelot, Redis, RabbitMQ, Kafka, Azure Service Bus, Docker, Kubernetes, JWT, circuit breakers, and distributed tracing start fitting into the same overall picture.

Your gateway is the front door of your APIs. Keep that door secure, resilient, observable, and simple.

Enjoyed this article? Share it with your network!