# API Gateway

Course: [API](https://www.devtutorials.ai/learn/building-apis) · Section: Production-Ready APIs
Source: https://www.devtutorials.ai/learn/building-apis/production-ready/api-gateway

> Put one front door in front of your services so auth, limits, routing and header rules happen once, in one place.

An **API gateway** is a single entry point that sits between clients and your services. Instead of exposing every service directly to the internet, clients send all requests to the gateway. The gateway performs common tasks such as authentication, rate limiting, routing, logging, and header processing before forwarding the request to the correct service. This helps keep security rules in one place, reduces duplicated code, and makes services easier to manage.

## Watch One Forgotten Check Leak Every Order

Brew & Bytes has grown into multiple services: the Menu Service, the Order Service and the Payment Service. Each service is exposed directly to the internet and is responsible for checking tokens on its own. Press **Run**.

Results:

- The menu service is public, so returning `200 OK` is expected.
- The payment service correctly checks for a token and returns `401 Unauthorized`.
- The order service forgot to perform the token check. Without any authentication, it returns every order, including customer information and totals.

**In simple words:** when every service is responsible for its own security checks, one forgotten check can expose sensitive data. A gateway moves those shared checks into a single place.

## TL;DR

- An **API gateway** is the single public entry point for clients.
- Clients call the gateway, not individual services.
- The gateway performs shared tasks such as authentication, authorization, rate limiting, routing, header processing, and logging.
- Services run in a private network and are not publicly accessible.
- The gateway can pass trusted user information to services.
- The gateway often returns `401 Unauthorized`, `429 Too Many Requests`, and `502 Bad Gateway`.
- Business rules belong inside services, not inside the gateway.

## A Real-World Example

Think about a large office building. Visitors enter through a reception desk. The receptionist checks their identity, issues a visitor badge, and directs them to the correct room.

Employees inside the building trust the visitor badge because it came from reception. They do not perform the full identity check again. The building also makes sure visitors cannot enter through side doors.

In this example:

| Building | API System |
|----------|----------|
| Reception desk | API gateway |
| Visitor badge | Trusted user headers |
| Office rooms | Services |
| Locked side doors | Private network |

## What You Will Learn

In this lesson, you will learn:

1. Why exposing services directly creates security risks.
2. How a gateway routes requests.
3. How a gateway performs authentication once.
4. How a gateway adds and removes headers.
5. How a gateway handles failures and rate limits.

A real gateway is usually its own application or service. In these examples, everything runs in a single program. The "services" are simple classes that the gateway calls directly. The concepts are exactly the same as a real distributed system.

## Routing: Only the Gateway Is Public

The gateway maintains a route table. The route table determines which service should receive each request.

For example, `/api/menu` goes to the Menu Service, and `/api/orders` goes to the Order Service. If the gateway does not recognize a route, it returns: `404 Not Found`

## Check the Token Once

Now the gateway checks the bearer token for every request. The service has no token code at all.

- No token: the gateway answers `401` with `WWW-Authenticate: Bearer`. The order service never runs.
- Token for customer 7: the gateway sets `X-User-Id: customer-7`, and the service returns only order 101.
- The last request is a trick. It has the token of customer 8 **and** it sends `X-User-Id: customer-7`, hoping to read the orders of Asha. The gateway ignores the header of the client and sets its own, so the reply is order 102 of customer 8.

A service may trust `X-User-Id` only because nobody except the gateway can reach it. The gateway must always **remove** that header from the incoming request, and it must write its own value.

## Header Transforms

Services often add headers for their own team: how long a query took, or which machine answered. Customers should not see these headers. The gateway is the right place to clean them up, and to add a **request id** that ties logs together.

- **Before** (`/no-transforms/...`): the reply leaks `X-Internal-Cost: 3` and `X-Service-Host: orders-pod-2.internal`. Now an attacker knows the names of your machines.
- **After** (`/api/...`): both internal headers are gone, and the gateway adds `X-Request-Id: req-0001`, and then `req-0002`. A real gateway uses a random id. This demo counts, so that the output never changes.

## When a Service Fails, and When a Caller Is Greedy

The gateway sees every request, so it is also the place to protect the services. It limits how often each caller may call, and it gives a clear error when a service is down.

- The key `app-ios` gets two calls (the limit is `2` on purpose, so that you can see it trip). The third call gets `429 Too Many Requests` with `Retry-After: 60`.
- The payment service is down. The gateway catches the failure and returns `502 Bad Gateway` as Problem Details. The client gets a clean, standard error, not a crash or a hang.

**Two rules for every gateway.** First, put no business logic in it: it must not work out prices, totals or who owns an order. Those rules belong in the services. Second, services must **not** be reachable directly from the internet. If they are, attackers simply skip the gateway and all its checks.

## A Real Gateway in .NET: YARP

In .NET, the common choice is **YARP** (Yet Another Reverse Proxy), a library from Microsoft. You list **routes** (which paths to match) and **clusters** (where the services live). This needs the `Yarp.ReverseProxy` NuGet package, so it is read-only here.

```csharp
// Program.cs (read-only: needs the Yarp.ReverseProxy package)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapReverseProxy();
app.Run();
```

```json
{
  "ReverseProxy": {
    "Routes": {
      "menu":   { "ClusterId": "menu",   "Match": { "Path": "/api/menu/{**rest}" } },
      "orders": { "ClusterId": "orders", "AuthorizationPolicy": "default",
                  "Match": { "Path": "/api/orders/{**rest}" } }
    },
    "Clusters": {
      "menu":   { "Destinations": { "d1": { "Address": "http://menu-svc:8080/" } } },
      "orders": { "Destinations": { "d1": { "Address": "http://orders-svc:8080/" } } }
    }
  }
}
```

Other well-known gateways are Azure API Management, AWS API Gateway, Kong and NGINX. They all do the same jobs that you just built by hand.

## Where You See This

- **Microservice apps** put a gateway in front, so that the phone app needs only one address.
- **Cloud platforms** offer managed gateways (Azure API Management, AWS API Gateway) with keys, limits and logs built in.
- **Kubernetes** clusters use an ingress or a gateway to send outside traffic to the right pods.
- **Public APIs**, such as payment or maps APIs, check your key and rate limit you at their gateway, before any service runs.

## Common mistakes

| Symptom | Cause | Fix |
|---|---|---|
| Data leaks even though the gateway checks tokens | The service is also reachable directly | Remove its public address, and allow only the gateway |
| A user reads the orders of another user | The gateway copied `X-User-Id` from the client | Drop the header of the client, and set your own |
| `/api/ordersecret` reaches the order service | The route matches with a plain `StartsWith` | Match the exact prefix, or the prefix plus `/` |
| Clients see `X-Service-Host` | There is no header transform on the way out | Strip internal headers at the gateway |
| The gateway changes for every new feature | Business logic moved into the gateway | Keep the gateway to shared jobs only |

## Exercises

## What to Remember

- An **API gateway** is the single public front door for all your services.
- It does the shared jobs once: **auth**, **rate limits**, **routing**, **header transforms**, and clean **errors**.
- Services live on a **private network**, and they trust the `X-User-Id` of the gateway.
- `502 Bad Gateway` when a service fails, and `429 Too Many Requests` when a caller is greedy.
- Put no business logic in the gateway. In .NET, **YARP** gives you routes and clusters out of the box.

**Next:** GraphQL. You will let clients ask for exactly the fields that they need, in one request, from one endpoint.

var app = builder.Build();

// No gateway: every service is open to the internet on its own address.
// Each service team must copy the token check into their own code.
var tokens = new Dictionary<string, string> { ["Bearer demo-token-7"] = "customer-7" };

// Menu service: public data, no check needed.
app.MapGet("/menu-service/menu", () =>
    Results.Ok(new[] { new { item = "Latte", price = 220 }, new { item = "Masala Chai", price = 120 } }));

// Payment service: remembered its copy of the token check.
app.MapGet("/payment-service/payments", (HttpContext context) =>
{
    if (!tokens.ContainsKey(context.Request.Headers["Authorization"].ToString()))
    {
        context.Response.Headers["WWW-Authenticate"] = "Bearer";
        return Results.StatusCode(401);
    }
    return Results.Ok(new[] { new { orderId = 101, amount = 220 } });
});

// Order service: the team FORGOT the token check. Anyone can read every order.
app.MapGet("/order-service/orders", () =>
    Results.Ok(new[]
    {
        new { id = 101, customerId = "customer-7", item = "Latte", total = 220 },
        new { id = 102, customerId = "customer-8", item = "Masala Chai", total = 120 },
    }));

app.Run();`;

// No gateway: every service is open to the internet on its own address.
// Each service team must copy the token check into their own code.
const tokens = new Map<string, string>([["Bearer demo-token-7", "customer-7"]]);

const app = new Hono();

// Menu service: public data, no check needed.
app.get("/menu-service/menu", (c) =>
  c.json([{ item: "Latte", price: 220 }, { item: "Masala Chai", price: 120 }], 200));

// Payment service: remembered its copy of the token check.
app.get("/payment-service/payments", (c) => {
  if (!tokens.has(c.req.header("Authorization") ?? "")) {
    c.header("WWW-Authenticate", "Bearer");
    return c.body(null, 401);
  }
  return c.json([{ orderId: 101, amount: 220 }], 200);
});

// Order service: the team FORGOT the token check. Anyone can read every order.
app.get("/order-service/orders", (c) =>
  c.json([
    { id: 101, customerId: "customer-7", item: "Latte", total: 220 },
    { id: 102, customerId: "customer-8", item: "Masala Chai", total: 120 },
  ], 200));

###
GET /payment-service/payments

###
GET /order-service/orders`;

var app = builder.Build();

// The services live inside the network. In this demo they are small classes.
// The gateway is the only door: it owns the route table.
var routes = new Dictionary<string, IService>
{
    ["menu"] = new MenuService(),
    ["orders"] = new OrderService(),
};

// Only /api/* is public. The services have no public URL at all.
app.MapGet("/api/{name}", (string name) =>
{
    if (!routes.TryGetValue(name, out var service))
        return Results.NotFound(new { error = "No route for /api/" + name });
    Reply reply = service.Handle("/" + name);
    return Results.Json(reply.Body, statusCode: reply.Status);
});

app.Run();

record Reply(int Status, object Body);
interface IService { Reply Handle(string path); }

class MenuService : IService
{
    public Reply Handle(string path) => new(200, new { service = "menu", items = new[] { "Latte", "Masala Chai" } });
}

class OrderService : IService
{
    public Reply Handle(string path) => new(200, new { service = "orders", ids = new[] { 101, 102 } });
}`;

type Reply = { status: number; body: unknown };
type Service = { handle: (path: string) => Reply };

// The services live inside the network. In this demo they are small objects.
const menuService: Service = {
  handle: () => ({ status: 200, body: { service: "menu", items: ["Latte", "Masala Chai"] } }),
};
const orderService: Service = {
  handle: () => ({ status: 200, body: { service: "orders", ids: [101, 102] } }),
};

// The gateway is the only door: it owns the route table.
const routes = new Map<string, Service>([["menu", menuService], ["orders", orderService]]);

const app = new Hono();

// Only /api/* is public. The services have no public URL at all.
app.get("/api/:name", (c) => {
  const name = c.req.param("name");
  const service = routes.get(name);
  if (!service) return c.json({ error: "No route for /api/" + name }, 404);
  const reply = service.handle("/" + name);
  return c.json(reply.body, reply.status as 200);
});

// Any other path (like /order-service/orders) does not exist on the internet.
app.notFound((c) => c.body(null, 404));

###
GET /api/orders

###
GET /api/x

###
GET /order-service/orders`;

var app = builder.Build();

var tokens = new Dictionary<string, string>
{
    ["Bearer demo-token-7"] = "customer-7",
    ["Bearer demo-token-8"] = "customer-8",
};
var orders = new OrderService();

// The gateway checks the token ONCE, for every service behind it.
app.MapGet("/api/orders", (HttpContext context) =>
{
    if (!tokens.TryGetValue(context.Request.Headers["Authorization"].ToString(), out var userId))
    {
        context.Response.Headers["WWW-Authenticate"] = "Bearer";
        return Results.StatusCode(401);
    }
    // Build the inner request. Never copy an X-User-Id the client sent:
    // only the gateway is allowed to set it.
    var inner = new Dictionary<string, string> { ["X-User-Id"] = userId };
    return Results.Ok(orders.Handle(inner));
});

app.Run();

// The order service has no token code. It trusts X-User-Id because
// only the gateway can reach it.
class OrderService
{
    readonly (int Id, string CustomerId, string Item)[] all = { (101, "customer-7", "Latte"), (102, "customer-8", "Masala Chai") };

    public object Handle(Dictionary<string, string> headers)
    {
        string userId = headers["X-User-Id"];
        var mine = all.Where(o => o.CustomerId == userId).Select(o => new { id = o.Id, item = o.Item });
        return new { userId, orders = mine };
    }
}`;

const tokens = new Map<string, string>([
  ["Bearer demo-token-7", "customer-7"],
  ["Bearer demo-token-8", "customer-8"],
]);

// The order service has no token code. It trusts X-User-Id because
// only the gateway can reach it.
const all = [
  { id: 101, customerId: "customer-7", item: "Latte" },
  { id: 102, customerId: "customer-8", item: "Masala Chai" },
];
const orderService = {
  handle(headers: Record<string, string>) {
    const userId = headers["X-User-Id"];
    const mine = all.filter((o) => o.customerId === userId).map((o) => ({ id: o.id, item: o.item }));
    return { userId, orders: mine };
  },
};

const app = new Hono();

// The gateway checks the token ONCE, for every service behind it.
app.get("/api/orders", (c) => {
  const userId = tokens.get(c.req.header("Authorization") ?? "");
  if (!userId) {
    c.header("WWW-Authenticate", "Bearer");
    return c.body(null, 401);
  }
  // Build the inner request. Never copy an X-User-Id the client sent:
  // only the gateway is allowed to set it.
  const inner = { "X-User-Id": userId };
  return c.json(orderService.handle(inner), 200);
});

###
GET /api/orders
Authorization: Bearer demo-token-7

###
GET /api/orders
Authorization: Bearer demo-token-8
X-User-Id: customer-7`;

var app = builder.Build();

var orders = new OrderService();
var internalHeaders = new[] { "X-Internal-Cost", "X-Service-Host" };
int counter = 0;

// BEFORE: what the client would see if we copied the service reply as it is.
app.MapGet("/no-transforms/orders/101", (HttpContext context) =>
{
    Reply reply = orders.Handle("/orders/101");
    foreach (var (name, value) in reply.Headers) context.Response.Headers[name] = value;
    return Results.Ok(reply.Body);
});

// AFTER: the gateway adds a request id and removes internal headers.
app.MapGet("/api/orders/101", (HttpContext context) =>
{
    counter++;
    string requestId = "req-" + counter.ToString("D4");
    Reply reply = orders.Handle("/orders/101");
    foreach (var (name, value) in reply.Headers)
    {
        if (!internalHeaders.Contains(name)) context.Response.Headers[name] = value;
    }
    context.Response.Headers["X-Request-Id"] = requestId;
    return Results.Ok(reply.Body);
});

app.Run();

record Reply(Dictionary<string, string> Headers, object Body);

// The service adds headers that help its own team, not the customer.
class OrderService
{
    public Reply Handle(string path) => new(
        new Dictionary<string, string> { ["X-Internal-Cost"] = "3", ["X-Service-Host"] = "orders-pod-2.internal" },
        new { id = 101, item = "Latte", total = 220 });
}`;

type Reply = { headers: Record<string, string>; body: unknown };

// The service adds headers that help its own team, not the customer.
const orderService = {
  handle: (path: string): Reply => ({
    headers: { "X-Internal-Cost": "3", "X-Service-Host": "orders-pod-2.internal" },
    body: { id: 101, item: "Latte", total: 220 },
  }),
};

const internalHeaders = ["X-Internal-Cost", "X-Service-Host"];
let counter = 0;

const app = new Hono();

// BEFORE: what the client would see if we copied the service reply as it is.
app.get("/no-transforms/orders/101", (c) => {
  const reply = orderService.handle("/orders/101");
  for (const [name, value] of Object.entries(reply.headers)) c.header(name, value);
  return c.json(reply.body, 200);
});

// AFTER: the gateway adds a request id and removes internal headers.
app.get("/api/orders/101", (c) => {
  counter++;
  const requestId = "req-" + String(counter).padStart(4, "0");
  const reply = orderService.handle("/orders/101");
  for (const [name, value] of Object.entries(reply.headers)) {
    if (!internalHeaders.includes(name)) c.header(name, value);
  }
  c.header("X-Request-Id", requestId);
  return c.json(reply.body, 200);
});

###
GET /api/orders/101

###
GET /api/orders/101`;

var app = builder.Build();

var menu = new MenuService();
var payments = new PaymentService();
var used = new Dictionary<string, int>();
const int Limit = 2; // tiny on purpose, so you can see it trip

app.MapGet("/api/{name}", (string name, HttpContext context) =>
{
    // 1. Limit: each API key gets 2 calls in this demo.
    string key = context.Request.Headers["X-Api-Key"].ToString();
    used[key] = used.GetValueOrDefault(key) + 1;
    if (used[key] > Limit)
    {
        context.Response.Headers["Retry-After"] = "60";
        return Results.Problem(statusCode: 429, title: "Too Many Requests",
            type: "https://tools.ietf.org/html/rfc6585#section-4", detail: "Key " + key + " used its 2 calls.");
    }
    // 2. Forward. If the service does not answer, say so with 502.
    try
    {
        object body = name == "payments" ? payments.Handle() : menu.Handle();
        return Results.Ok(body);
    }
    catch (HttpRequestException)
    {
        return Results.Problem(statusCode: 502, title: "Bad Gateway",
            type: "https://tools.ietf.org/html/rfc9110#section-15.6.3", detail: "The " + name + " service did not answer.");
    }
});

app.Run();

class MenuService { public object Handle() => new { items = new[] { "Latte", "Cold Coffee" } }; }

// The payment service is down. In real life the network call would fail.
class PaymentService
{
    public object Handle() => throw new HttpRequestException("Connection refused");
}`;

// Problem Details (RFC 9457): one standard shape for every error.
function problem(c: Context, status: number, title: string, type: string, detail: string) {
  const body = { type, title, status, detail };
  return c.body(JSON.stringify(body), status as 429, { "Content-Type": "application/problem+json" });
}

const menuService = { handle: () => ({ items: ["Latte", "Cold Coffee"] }) };

// The payment service is down. In real life the network call would fail.
const paymentService = {
  handle: (): unknown => { throw new Error("Connection refused"); },
};

const used = new Map<string, number>();
const LIMIT = 2; // tiny on purpose, so you can see it trip

const app = new Hono();

app.get("/api/:name", (c) => {
  const name = c.req.param("name");
  // 1. Limit: each API key gets 2 calls in this demo.
  const key = c.req.header("X-Api-Key") ?? "";
  used.set(key, (used.get(key) ?? 0) + 1);
  if (used.get(key)! > LIMIT) {
    c.header("Retry-After", "60");
    return problem(c, 429, "Too Many Requests", "https://tools.ietf.org/html/rfc6585#section-4", "Key " + key + " used its 2 calls.");
  }
  // 2. Forward. If the service does not answer, say so with 502.
  try {
    const body = name === "payments" ? paymentService.handle() : menuService.handle();
    return c.json(body, 200);
  } catch {
    return problem(c, 502, "Bad Gateway", "https://tools.ietf.org/html/rfc9110#section-15.6.3", "The " + name + " service did not answer.");
  }
});

X-Api-Key: app-ios

###
GET /api/menu
X-Api-Key: app-ios

###
GET /api/menu
X-Api-Key: app-ios

###
GET /api/payments
X-Api-Key: app-web`;

  {
    prompt: "The route matcher sends /api/ordersecret to OrderService. Make it match only the exact prefix, or the prefix followed by a slash, so that this path gets 404.",
    hint: "Compare path == prefix, or check StartsWith(prefix + \"/\").",
    explain: "A plain StartsWith treats /api/ordersecret as part of /api/orders. Adding the slash makes the match stop at a real path segment, so /api/orders/101 still works and /api/ordersecret is 404.",
    csharp: { starter: `// The gateway route table: a path prefix and the service that handles it.
var routes = new (string Prefix, string Service)[]
{
    ("/api/menu", "MenuService"),
    ("/api/orders", "OrderService"),
    ("/api/payments", "PaymentService"),
};

string Match(string path)
{
    foreach (var (prefix, service) in routes)
    {
        // TODO: "/api/ordersecret" should be 404, but it matches orders.
        // Fix: match only the exact prefix, or the prefix followed by "/".
        if (path.StartsWith(prefix)) return service;
    }
    return "404";
}

foreach (var path in new[] { "/api/menu", "/api/orders/101", "/api/ordersecret", "/menu-service/menu", "/api/loyalty" })
    Console.WriteLine(path + " -> " + Match(path));`, solution: `// The gateway route table: a path prefix and the service that handles it.
var routes = new (string Prefix, string Service)[]
{
    ("/api/menu", "MenuService"),
    ("/api/orders", "OrderService"),
    ("/api/payments", "PaymentService"),
};

string Match(string path)
{
    foreach (var (prefix, service) in routes)
    {
        // Match the exact prefix, or the prefix followed by "/".
        if (path == prefix || path.StartsWith(prefix + "/")) return service;
    }
    return "404";
}

foreach (var path in new[] { "/api/menu", "/api/orders/101", "/api/ordersecret", "/menu-service/menu", "/api/loyalty" })
    Console.WriteLine(path + " -> " + Match(path));`, mode: "console" },
    react: { starter: `// The gateway route table: a path prefix and the service that handles it.
const routes: [string, string][] = [
  ["/api/menu", "MenuService"],
  ["/api/orders", "OrderService"],
  ["/api/payments", "PaymentService"],
];

function match(path: string): string {
  for (const [prefix, service] of routes) {
    // TODO: "/api/ordersecret" should be 404, but it matches orders.
    // Fix: match only the exact prefix, or the prefix followed by "/".
    if (path.startsWith(prefix)) return service;
  }
  return "404";
}

for (const path of ["/api/menu", "/api/orders/101", "/api/ordersecret", "/menu-service/menu", "/api/loyalty"])
  console.log(path + " -> " + match(path));`, solution: `// The gateway route table: a path prefix and the service that handles it.
const routes: [string, string][] = [
  ["/api/menu", "MenuService"],
  ["/api/orders", "OrderService"],
  ["/api/payments", "PaymentService"],
];

function match(path: string): string {
  for (const [prefix, service] of routes) {
    // Match the exact prefix, or the prefix followed by "/".
    if (path === prefix || path.startsWith(prefix + "/")) return service;
  }
  return "404";
}

for (const path of ["/api/menu", "/api/orders/101", "/api/ordersecret", "/menu-service/menu", "/api/loyalty"])
  console.log(path + " -> " + match(path));`, mode: "console" }
  },
  {
    prompt: "The LoyaltyService is written, but /api/loyalty returns 404. Add one entry to the gateway route table, so that it reaches the service.",
    hint: "Add [\"loyalty\"] = new LoyaltyService() to the routes dictionary.",
    explain: "The gateway only knows the routes in its table. One new line makes the service public through the gateway, and the service itself stays private.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// The services live inside the network. In this demo they are small classes.
// The gateway is the only door: it owns the route table.
var routes = new Dictionary<string, IService>
{
    ["menu"] = new MenuService(),
    ["orders"] = new OrderService(),
    // TODO: add the route "loyalty" so /api/loyalty goes to LoyaltyService.
};

// Only /api/* is public. The services have no public URL at all.
app.MapGet("/api/{name}", (string name) =>
{
    if (!routes.TryGetValue(name, out var service))
        return Results.NotFound(new { error = "No route for /api/" + name });
    Reply reply = service.Handle("/" + name);
    return Results.Json(reply.Body, statusCode: reply.Status);
});

app.Run();

record Reply(int Status, object Body);
interface IService { Reply Handle(string path); }

class MenuService : IService
{
    public Reply Handle(string path) => new(200, new { service = "menu", items = new[] { "Latte", "Masala Chai" } });
}

class OrderService : IService
{
    public Reply Handle(string path) => new(200, new { service = "orders", ids = new[] { 101, 102 } });
}

// The new service is ready, but the gateway does not know about it yet.
class LoyaltyService : IService
{
    public Reply Handle(string path) => new(200, new { service = "loyalty", customerId = "customer-7", points = 340 });
}`, solution: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// The services live inside the network. In this demo they are small classes.
// The gateway is the only door: it owns the route table.
var routes = new Dictionary<string, IService>
{
    ["menu"] = new MenuService(),
    ["orders"] = new OrderService(),
    ["loyalty"] = new LoyaltyService(), // the new route
};

// Only /api/* is public. The services have no public URL at all.
app.MapGet("/api/{name}", (string name) =>
{
    if (!routes.TryGetValue(name, out var service))
        return Results.NotFound(new { error = "No route for /api/" + name });
    Reply reply = service.Handle("/" + name);
    return Results.Json(reply.Body, statusCode: reply.Status);
});

app.Run();

record Reply(int Status, object Body);
interface IService { Reply Handle(string path); }

class MenuService : IService
{
    public Reply Handle(string path) => new(200, new { service = "menu", items = new[] { "Latte", "Masala Chai" } });
}

class OrderService : IService
{
    public Reply Handle(string path) => new(200, new { service = "orders", ids = new[] { 101, 102 } });
}

// The new service. One line in the route table makes it public.
class LoyaltyService : IService
{
    public Reply Handle(string path) => new(200, new { service = "loyalty", customerId = "customer-7", points = 340 });
}`, mode: "aspnet", requests: `GET /api/menu

###
GET /api/loyalty` },
    react: { starter: `import { Hono } from "hono";

type Reply = { status: number; body: unknown };
type Service = { handle: (path: string) => Reply };

// The services live inside the network. In this demo they are small objects.
const menuService: Service = {
  handle: () => ({ status: 200, body: { service: "menu", items: ["Latte", "Masala Chai"] } }),
};
const orderService: Service = {
  handle: () => ({ status: 200, body: { service: "orders", ids: [101, 102] } }),
};

// The new service is ready, but the gateway does not know about it yet.
const loyaltyService: Service = {
  handle: () => ({ status: 200, body: { service: "loyalty", customerId: "customer-7", points: 340 } }),
};

// The gateway is the only door: it owns the route table.
const routes = new Map<string, Service>([
  ["menu", menuService],
  ["orders", orderService],
  // TODO: add the route "loyalty" so /api/loyalty goes to loyaltyService.
]);

const app = new Hono();

// Only /api/* is public. The services have no public URL at all.
app.get("/api/:name", (c) => {
  const name = c.req.param("name");
  const service = routes.get(name);
  if (!service) return c.json({ error: "No route for /api/" + name }, 404);
  const reply = service.handle("/" + name);
  return c.json(reply.body, reply.status as 200);
});

type Reply = { status: number; body: unknown };
type Service = { handle: (path: string) => Reply };

// The services live inside the network. In this demo they are small objects.
const menuService: Service = {
  handle: () => ({ status: 200, body: { service: "menu", items: ["Latte", "Masala Chai"] } }),
};
const orderService: Service = {
  handle: () => ({ status: 200, body: { service: "orders", ids: [101, 102] } }),
};

// The new service. One line in the route table makes it public.
const loyaltyService: Service = {
  handle: () => ({ status: 200, body: { service: "loyalty", customerId: "customer-7", points: 340 } }),
};

// The gateway is the only door: it owns the route table.
const routes = new Map<string, Service>([
  ["menu", menuService],
  ["orders", orderService],
  ["loyalty", loyaltyService], // the new route
]);

const app = new Hono();

// Only /api/* is public. The services have no public URL at all.
app.get("/api/:name", (c) => {
  const name = c.req.param("name");
  const service = routes.get(name);
  if (!service) return c.json({ error: "No route for /api/" + name }, 404);
  const reply = service.handle("/" + name);
  return c.json(reply.body, reply.status as 200);
});

###
GET /api/loyalty` },
    checks: `GET /api/orders

###
GET /api/rewards`
  },
  {
    prompt: "X-Internal-Cost still reaches the customer. Make the gateway strip it, in the same way that it already strips X-Service-Host.",
    hint: "Add \"X-Internal-Cost\" to the internalHeaders list.",
    explain: "The gateway copies every header except the ones on its block list. With X-Internal-Cost on the list, only Cache-Control and the body reach the client.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var orders = new OrderService();
// TODO: X-Internal-Cost still reaches the customer. Add it to this list.
var internalHeaders = new[] { "X-Service-Host" };

app.MapGet("/api/orders/101", (HttpContext context) =>
{
    Reply reply = orders.Handle("/orders/101");
    foreach (var (name, value) in reply.Headers)
    {
        if (!internalHeaders.Contains(name)) context.Response.Headers[name] = value;
    }
    return Results.Ok(reply.Body);
});

app.Run();

record Reply(Dictionary<string, string> Headers, object Body);

class OrderService
{
    public Reply Handle(string path) => new(
        new Dictionary<string, string> { ["X-Internal-Cost"] = "3", ["X-Service-Host"] = "orders-pod-2.internal", ["Cache-Control"] = "no-store" },
        new { id = 101, item = "Latte", total = 220 });
}`, solution: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var orders = new OrderService();
// Headers only our own teams need. The customer never sees them.
var internalHeaders = new[] { "X-Internal-Cost", "X-Service-Host" };

app.MapGet("/api/orders/101", (HttpContext context) =>
{
    Reply reply = orders.Handle("/orders/101");
    foreach (var (name, value) in reply.Headers)
    {
        if (!internalHeaders.Contains(name)) context.Response.Headers[name] = value;
    }
    return Results.Ok(reply.Body);
});

app.Run();

record Reply(Dictionary<string, string> Headers, object Body);

class OrderService
{
    public Reply Handle(string path) => new(
        new Dictionary<string, string> { ["X-Internal-Cost"] = "3", ["X-Service-Host"] = "orders-pod-2.internal", ["Cache-Control"] = "no-store" },
        new { id = 101, item = "Latte", total = 220 });
}`, mode: "aspnet", requests: `GET /api/orders/101` },
    react: { starter: `import { Hono } from "hono";

type Reply = { headers: Record<string, string>; body: unknown };

const orderService = {
  handle: (path: string): Reply => ({
    headers: { "X-Internal-Cost": "3", "X-Service-Host": "orders-pod-2.internal", "Cache-Control": "no-store" },
    body: { id: 101, item: "Latte", total: 220 },
  }),
};

// TODO: X-Internal-Cost still reaches the customer. Add it to this list.
const internalHeaders = ["X-Service-Host"];

const app = new Hono();

app.get("/api/orders/101", (c) => {
  const reply = orderService.handle("/orders/101");
  for (const [name, value] of Object.entries(reply.headers)) {
    if (!internalHeaders.includes(name)) c.header(name, value);
  }
  return c.json(reply.body, 200);
});

type Reply = { headers: Record<string, string>; body: unknown };

const orderService = {
  handle: (path: string): Reply => ({
    headers: { "X-Internal-Cost": "3", "X-Service-Host": "orders-pod-2.internal", "Cache-Control": "no-store" },
    body: { id: 101, item: "Latte", total: 220 },
  }),
};

// Headers only our own teams need. The customer never sees them.
const internalHeaders = ["X-Internal-Cost", "X-Service-Host"];

const app = new Hono();

app.get("/api/orders/101", (c) => {
  const reply = orderService.handle("/orders/101");
  for (const [name, value] of Object.entries(reply.headers)) {
    if (!internalHeaders.includes(name)) c.header(name, value);
  }
  return c.json(reply.body, 200);
});

    checks: `GET /api/orders/101?view=full

###
GET /api/orders/101
Accept: application/json`
  }
];
