# API Cheat Sheet

Course: [API](https://www.devtutorials.ai/learn/building-apis) · Section: Reference
Source: https://www.devtutorials.ai/learn/building-apis/reference/cheat-sheet

> One page with the methods, URL rules, error shape, headers and side-by-side C# and Hono snippets you reach for every day.

The **API Cheat Sheet** is a quick reference guide that brings together the most important API rules, patterns, headers, status codes, and code examples used throughout this course. Keep it nearby while building APIs. Instead of searching through multiple lessons, you can use this page to quickly remember common practices, correct response codes, useful headers, validation patterns, and API design rules. The examples use the Brew & Bytes coffee shop API, but the same principles apply to almost every REST API you build.

## HTTP Methods

| Method | Use It For | Safe? | Idempotent? | Typical Success Code |
|---|---|---|---|---|
| `GET` | Read data | Yes | Yes | `200 OK` or `304 Not Modified` |
| `HEAD` | Read only response headers | Yes | Yes | `200 OK` |
| `OPTIONS` | Check supported methods or perform a CORS preflight request | Yes | Yes | `204 No Content` |
| `POST` | Create a resource or start an action | No | No | `201 Created` |
| `PUT` | Replace an entire resource | No | Yes | `200 OK` or `204 No Content` |
| `PATCH` | Update part of a resource | No | Not always | `200 OK` |
| `DELETE` | Remove a resource | No | Yes | `204 No Content` |

### Quick Rules

- **Safe** means the request does not change data.
- **Idempotent** means sending the same request multiple times produces the same final result.
- Always use an **Idempotency-Key** when retrying critical POST requests such as payments.
- Never use `GET` to create, update, or delete data.

See: **HTTP Methods**

## URL Design Rules

| Rule | Good | Bad |
|---|---|---|
| Use nouns, not verbs | `/orders` | `/getOrders` |
| Use plural resource names | `/customers` | `/customer` |
| Use lowercase URLs | `/menu-items` | `/MenuItems` |
| Use hyphens between words | `/menu-items` | `/menu_items` |
| Put ids in the path | `/orders/105` | `/orders?id=105` |
| Use query strings for filters | `/orders?status=open` | `/open-orders` |
| Keep nesting simple | `/customers/7/orders` | `/customers/7/orders/105/items/2/notes` |

### Pagination

Page numbers: `/orders?page=2&pageSize=20`

Cursor pagination: `/orders?after=105&limit=20`

Always enforce a maximum page size.

See: **REST API Design**

## Problem Details (Standard Error Format)

All API errors should use the same response format.

Content type: `Content-Type: application/problem+json`

Example: `{"type": "https://api.brewbytes.dev/problems/validation", "title": "Your order has some problems", "status": 422, "detail": "Fix the fields below and try again.", "instance": "/orders", "errors": {"quantity": ["Quantity must be between 1 and 10."], "item": ["We do not sell Mocha Frappe."]}}`

### Common Fields

| Field | Purpose |
|---|---|
| `type` | Identifies the type of problem |
| `title` | Short error summary |
| `status` | HTTP status code |
| `detail` | Specific description of the error |
| `instance` | Path that caused the error |
| `errors` | Validation errors grouped by field |

### Important

Do not expose stack traces, SQL queries, or internal exception details. Log the technical information on the server instead.

See: **Status Codes** and **Validation**

## API Versioning Headers

| Header | Direction | Example | Purpose |
|---|---|---|---|
| `X-Api-Version` | Request | `X-Api-Version: 2` | Requests a specific API version |
| `Deprecation` | Response | `Deprecation: true` | Marks a version as deprecated |
| `Sunset` | Response | `Sunset: Sat, 31 Jan 2027 00:00:00 GMT` | Removal date |
| `Link` | Response | `Link: </v2/orders>; rel="successor-version"` | Link to next version |

### Breaking Changes

The following changes usually require a new API version: renaming a field, removing a field, changing a field type, adding a required request field, and removing an endpoint.

### Safe Changes

The following changes are usually backward compatible: adding optional fields, adding new endpoints, and adding optional query parameters.

See: **API Versioning**

## Authentication Headers

### API Key

Header: `X-Api-Key: bb_live_7f3a...`

Used for partner integrations and server-to-server communication.

### Bearer Token

Header: `Authorization: Bearer eyJhbGciOi...`

Used for user authentication and JWT access tokens.

### Authentication Challenge

Header: `WWW-Authenticate: Bearer error="invalid_token"`

Returned with: `401 Unauthorized`

### Quick Rules

- Invalid or missing credentials → `401 Unauthorized`
- User authenticated but not allowed → `403 Forbidden`
- Accessing another user's resource → often return `404 Not Found`
- Always validate JWT signature, expiration, issuer, and audience

See: **Authentication and Authorization**

## Cache Headers

| Header | Example | Purpose |
|---|---|---|
| `Cache-Control` | `public, max-age=60` | Cache for 60 seconds |
| `Cache-Control` | `private, no-store` | Do not cache |
| `ETag` | `"menu-12"` | Version identifier |
| `If-None-Match` | `"menu-12"` | Client cache validation |
| `Vary` | `X-Api-Version` | Cache based on header values |

### Cache Example

Server reply: `ETag: "menu-12"` and `Cache-Control: public, max-age=60`

Client request: `If-None-Match: "menu-12"`

Server response: `304 Not Modified`

No body is returned because the client's copy is already up to date.

See: **Performance and Caching**

## Rate Limiting Headers

| Header | Example | Purpose |
|---|---|---|
| `RateLimit-Limit` | `100` | Maximum requests allowed |
| `RateLimit-Remaining` | `12` | Requests remaining |
| `RateLimit-Reset` | `30` | Seconds until reset |
| `Retry-After` | `30` | Wait time before retrying |

### Retry Guidelines

Retry these: `429 Too Many Requests`, `502 Bad Gateway`, `503 Service Unavailable` and `504 Gateway Timeout`.

Do not retry these: `400 Bad Request`, `401 Unauthorized`, `403 Forbidden` and `404 Not Found`.

If `Retry-After` is present, wait exactly that long. Otherwise use **exponential backoff**. See: **Rate Limits and Resilience**

## Most Common Response Codes

| Status | Meaning |
|---|---|
| `200 OK` | Request succeeded |
| `201 Created` | Resource created |
| `202 Accepted` | Processing started |
| `204 No Content` | Success with no body |
| `304 Not Modified` | Use cached copy |
| `400 Bad Request` | Invalid request |
| `401 Unauthorized` | Authentication required |
| `403 Forbidden` | Permission denied |
| `404 Not Found` | Resource not found |
| `405 Method Not Allowed` | HTTP method not supported |
| `409 Conflict` | Request conflicts with current state |
| `415 Unsupported Media Type` | Unsupported body format |
| `422 Unprocessable Content` | Validation failed |
| `429 Too Many Requests` | Rate limit exceeded |
| `500 Internal Server Error` | Unexpected server error |
| `502 Bad Gateway` | Upstream service error |
| `503 Service Unavailable` | Temporary service outage |
| `504 Gateway Timeout` | Upstream timeout |

## C# and Hono Side by Side

Each pair does the same job. The C# side is an ASP.NET Core minimal API in `Program.cs`. The TypeScript side is Hono in `server.ts`.

### Define a route

```csharp
app.MapGet("/menu", () => Results.Ok(menu));
app.MapGet("/orders/{id:int}", (int id) => Results.Ok(FindOrder(id)));
```

```ts
app.get("/menu", (c) => c.json(menu, 200));
app.get("/orders/:id", (c) => c.json(findOrder(Number(c.req.param("id"))), 200));
```

### Read a query string

```csharp
app.MapGet("/orders", (string? status, int limit = 20) =>
    Results.Ok(orders.Where(o => status == null || o.Status == status).Take(Math.Min(limit, 100))));
```

```ts
app.get("/orders", (c) => {
  const status = c.req.query("status");
  const limit = Math.min(Number(c.req.query("limit") ?? 20), 100);
  return c.json(orders.filter((o) => !status || o.status === status).slice(0, limit), 200);
});
```

### Read a JSON body into a DTO

```csharp
record CreateOrder(string Item, int Quantity);

app.MapPost("/orders", (CreateOrder input) => { /* use input.Item, input.Quantity */ });
```

```ts
import { z } from "zod";
const CreateOrder = z.object({ item: z.string(), quantity: z.number().int().min(1).max(10) });

app.post("/orders", async (c) => {
  const input = CreateOrder.parse(await c.req.json());
  // use input.item, input.quantity
});
```

### Return 201 Created with a Location

```csharp
var order = new Order(nextId++, input.Item, input.Quantity, "placed");
orders.Add(order);
return Results.Created($"/orders/{order.Id}", order);
```

```ts
const order = { id: nextId++, item: input.item, quantity: input.quantity, status: "placed" };
orders.push(order);
return c.json(order, 201, { Location: `/orders/${order.id}` });
```

### Return a Problem Details error

```csharp
return Results.Problem(statusCode: 409, title: "Too late to cancel",
    detail: "The kitchen has started this order.");
```

```ts
const problem = { type: "about:blank", title: "Too late to cancel", status: 409,
  detail: "The kitchen has started this order." };
return c.body(JSON.stringify(problem), 409, { "Content-Type": "application/problem+json" });
```

### Return validation errors (422)

```csharp
return Results.ValidationProblem(
    new Dictionary<string, string[]> { ["quantity"] = ["Quantity must be between 1 and 10."] },
    statusCode: 422);
```

```ts
const result = CreateOrder.safeParse(await c.req.json());
if (!result.success) {
  const body = { title: "Your order has some problems", status: 422,
    errors: result.error.flatten().fieldErrors };
  return c.body(JSON.stringify(body), 422, { "Content-Type": "application/problem+json" });
}
```

### Read and set a header

```csharp
app.MapGet("/orders/{id:int}", (int id, HttpRequest req, HttpResponse res) =>
{
    var version = req.Headers["X-Api-Version"].ToString();
    res.Headers["X-Correlation-Id"] = Guid.NewGuid().ToString();
    return Results.Ok(FindOrder(id));
});
```

```ts
app.get("/orders/:id", (c) => {
  const version = c.req.header("X-Api-Version") ?? "1";
  c.header("X-Correlation-Id", crypto.randomUUID());
  return c.json(findOrder(Number(c.req.param("id"))), 200);
});
```

### Check auth and ownership

```csharp
app.MapGet("/orders/{id:int}", (int id, ClaimsPrincipal user) =>
{
    var order = FindOrder(id);
    if (order is null || order.CustomerId != user.FindFirstValue("sub"))
        return Results.NotFound();          // 404 hides other people's orders
    return Results.Ok(order);
}).RequireAuthorization();                   // no valid token: 401
```

```ts
app.get("/orders/:id", async (c) => {
  const token = c.req.header("Authorization")?.replace("Bearer ", "");
  const claims = token ? await verifyJwt(token) : null;   // checks signature, exp, iss, aud
  if (!claims) return c.body(null, 401, { "WWW-Authenticate": "Bearer" });
  const order = findOrder(Number(c.req.param("id")));
  if (!order || order.customerId !== claims.sub) return c.body(null, 404);
  return c.json(order, 200);
});
```

### Send an ETag and answer 304

```csharp
app.MapGet("/menu", (HttpRequest req, HttpResponse res) =>
{
    var etag = $"\"menu-{menuVersion}\"";
    if (req.Headers.IfNoneMatch == etag) return Results.StatusCode(304);
    res.Headers.ETag = etag;
    res.Headers.CacheControl = "public, max-age=60";
    return Results.Ok(menu);
});
```

```ts
app.get("/menu", (c) => {
  const etag = `"menu-${menuVersion}"`;
  if (c.req.header("If-None-Match") === etag) return c.body(null, 304);
  return c.json(menu, 200, { ETag: etag, "Cache-Control": "public, max-age=60" });
});
```

### Return 429 with Retry-After

```csharp
builder.Services.AddRateLimiter(o =>
{
    o.RejectionStatusCode = 429;
    o.AddFixedWindowLimiter("perKey", w => { w.PermitLimit = 100; w.Window = TimeSpan.FromMinutes(1); });
});
app.MapGet("/menu", () => Results.Ok(menu)).RequireRateLimiting("perKey");
```

```ts
if (used >= 100) {
  const body = { title: "Too Many Requests", status: 429, detail: "Try again in 30 seconds." };
  return c.body(JSON.stringify(body), 429,
    { "Content-Type": "application/problem+json", "Retry-After": "30" });
}
```

In C#, `Results.Problem(...)` sets `application/problem+json` for you. In Hono you set it yourself, so a small `problem(c, status, title, detail)` helper keeps every error the same.

## Which API Style Should You Choose?

| Need | Recommended Style |
|---|---|
| Standard CRUD APIs | REST |
| Flexible client-driven data queries | GraphQL |
| High-performance service-to-service communication | gRPC |
| Event notifications to external systems | Webhooks |
| Real-time browser updates | Server-Sent Events (SSE) |

### General Recommendation

Choose **REST** by default. Use GraphQL, gRPC, Webhooks, or SSE only when their specific strengths are needed. See: **REST vs GraphQL vs gRPC**
