---
title: "03 — URLs, DNS, and the Browser Rules: API Foundations"
uid: api-foundations-url-dns-cors
tags: ["dns", "url", "content-negotiation", "cookies", "cors", "roadmap:api-design", "api", "caching"]
excerpt: "Before your endpoint logic ever runs, a URL must survive DNS resolution, a TCP connection, and a gauntlet of browser-enforced rules — CORS, cookies, content negotiation, caching. The foundation decides whether the call even reaches your code."
date: 2026-08-13T03:28:34+0000
source: https://www.aveshina.my.id/en/blog/api-foundations-url-dns-cors
---

I used to skip straight to writing handlers, and the layer underneath — URLs, DNS, TCP/IP, cookies, CORS, content negotiation, caching — was invisible until a request silently died there. The model that finally stuck: **an API call is a URL resolved through DNS into a TCP connection, and a handful of protocol and browser rules (CORS, cookies, content negotiation, caching) decide whether the request is even allowed to reach your code.** [1][5] The endpoint logic sits on top of all that; if the foundation rejects the call, the handler never runs.

The reason this matters as a unit is that these nodes are not independent features. They are the gates every request passes through, in order — *find the server, open the connection, satisfy the browser, negotiate the format, reuse the cache*. Getting any of them wrong looks like "the API is broken" when really it's a gate upstream of the handler.

## The URL: four parts, four jobs

Every API call starts as a URL, and the URL is not one thing — it's four segments with distinct roles [1]:

```figure
<svg viewBox="0 0 720 130" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Anatomy of an API URL split into four labeled segments: scheme https, host api.example.com, path /orders/42, and query ?status=active. Each segment is color-coded and labeled with its job.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <g font-family="ui-monospace, monospace" font-size="15" font-weight="700" text-anchor="middle" dominant-baseline="middle">
      <rect x="20" y="30" width="90" height="40" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
      <text x="65" y="51" fill="#1e1b4b">https://</text>
      <rect x="120" y="30" width="190" height="40" rx="8" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
      <text x="215" y="51" fill="#500724">api.example.com</text>
      <rect x="320" y="30" width="130" height="40" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
      <text x="385" y="51" fill="#052e16">/orders/42</text>
      <rect x="460" y="30" width="240" height="40" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
      <text x="580" y="51" fill="#422006">?status=active</text>
    </g>
    <g font-size="11" font-weight="700" text-anchor="middle">
      <text x="65" y="92" fill="#1e1b4b">scheme</text>
      <text x="215" y="92" fill="#500724">host</text>
      <text x="385" y="92" fill="#052e16">path</text>
      <text x="580" y="92" fill="#422006">query</text>
    </g>
    <g font-size="10" fill="#64748b" text-anchor="middle">
      <text x="65" y="110">encryption</text>
      <text x="215" y="110">who to ask (→ DNS)</text>
      <text x="385" y="110">which resource (+ path params)</text>
      <text x="580" y="110">filtering / sorting</text>
    </g>
  </g>
</svg>
```

- **Scheme** (https) — whether the conversation is encrypted. https is HTTP wrapped in TLS (the encryption layer); http is plaintext. There is no third option worth shipping.
- **Host** (api.example.com) — the machine. This is the only segment DNS cares about.
- **Path** (/orders/42) — the resource. **Path parameters** (42) identify *which* resource [1].
- **Query** (?status=active) — **query parameters** filter, sort, or shape the response — they don't identify the resource [1].

The discipline that came out of seeing them separately: the path names *what*, the query describes *how much/in what order*. Mixing them — putting id in the query, or a filter in the path — breaks caching and routing in confusing ways, because intermediaries key on the path and treat the query as secondary.

## DNS: turning the host into a number

The host is a name; the network only routes by number. **DNS** is the distributed phonebook that resolves api.example.com to an IP address, consulted in milliseconds before every connection [2]. The lookup fans out through a hierarchy — recursive resolver, root, TLD, authoritative — and each level gets cached, so most calls never make the full trip.

For API design the practical points are small but real. First, DNS is the first thing to suspect when a call fails intermittently from one network but works from another — a stale or misconfigured record. Second, DNS is *why an API can move servers without breaking clients*: the host name stays fixed while the IP underneath changes. The host is the stable identity; the IP is an implementation detail.

## TCP/IP: the connection underneath

Below DNS is **TCP/IP** — the suite that actually moves the bytes [3]. Two ideas from this layer matter for API behavior. **IP** routes packets by address. **TCP** opens a reliable, ordered, error-checked connection on top of IP and hands the application a clean byte stream — which is what HTTP rides on.

The detail I had to internalize: every HTTP request opens (or reuses) a TCP connection, and that handshake has real latency. This is the reason HTTP/2 multiplexes many requests over one connection and the reason connection pools exist — both are workarounds for "TCP handshakes are expensive." An API that makes the client open a fresh connection per call will feel slow even when the handler is fast.

## Cookies: bolting state onto stateless HTTP

HTTP is stateless, but real sessions need memory. **Cookies** are the workaround — small key/value pairs the server sets via the Set-Cookie response header and the browser stores and replays on every subsequent request to that origin [4]. The classic use is a session ID: the server issues a cookie after login, and every later request carries it, so the server can look up "who is this" without re-authenticating.

Two things I now watch for. Cookies are scoped to an origin and have security flags — HttpOnly (no JS access), Secure (HTTPS only), SameSite (cross-site rules). Getting those wrong is how session-cookie theft happens. And cookies are a *browser* mechanism; a server-to-server API call generally doesn't use them — it uses a token in a header. Mixing the two models is a common source of auth bugs.

## CORS: the browser's cross-origin gate

**CORS** (Cross-Origin Resource Sharing) is the rule that trips up every frontend dev the first time, and the model that made it click is: **CORS is enforced by the browser, not the server.** [5] By default, a browser will not let JavaScript on app.example.com read a response from api.other.com. The server has to explicitly opt in by returning CORS headers (Access-Control-Allow-Origin, etc.) that say "this origin may read me."

```
// browser preflight (OPTIONS) → server response
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
```

The trap: a request failing with a CORS error from the browser but succeeding from curl is not a contradiction — curl is not a browser and doesn't enforce CORS. The server received and maybe even handled the request; the browser just refused to hand the response to your script. When debugging "CORS issues," the fix is almost always on the server's CORS headers, not the client.

## Content negotiation: same resource, different shape

The same resource can have multiple representations — JSON, XML, HTML. **Content negotiation** is how the client and server agree on which one [6]. The client declares a preference with the Accept header; the server picks a representation and confirms it with Content-Type. Most modern APIs hardcode JSON and skip negotiation, but the mechanism is still there: one resource, many shapes, chosen per request. The lesson I keep is that the *resource* and its *representation* are separate ideas — which is the whole point of REST's decoupling.

## HTTP caching: not re-doing work you already did

**Caching** stores copies of responses so repeated requests don't re-hit the server [7]. It's governed by headers, the key ones being Cache-Control (how long, who may cache) and ETag/If-None-Match (a fingerprint for conditional requests). The flow that clicked:

```
// first response
Cache-Control: max-age=300
ETag: "v3"

// later request, if cached entry exists
If-None-Match: "v3"

// server, if nothing changed
304 Not Modified          // no body — reuse the cached copy
```

A 304 is the API saying "your cached copy is still valid" and saving the bytes of the body. The design rule: **GET responses are cacheable; mutating responses generally aren't.** That's another reason verb choice matters — caches key off the assumption that GET is safe and idempotent. A GET with side effects doesn't just break semantics, it poisons every cache between client and server.

## How I use this

When an API call fails before the handler logs anything, I walk the gates in order. Can DNS resolve the host? Did TCP even connect (or is it a TLS/firewall issue)? If it's a browser call, is CORS blocking the response? Did the client send the right Accept/Content-Type? Is a stale cache serving a 304 or an old copy? Almost every "the API is down" that isn't actually a 5xx lives in one of those gates. Naming the gate before reaching for the handler is the whole habit — the foundation rejects far more requests than the endpoint ever does.

## References

[1] Treblle, "How API Parameters Work: Query, Path, Header, and Body," 2024. [Online]. Available: [https://treblle.com/blog/api-parameters-query-path-header-body](https://treblle.com/blog/api-parameters-query-path-header-body)

[2] Cloudflare, "What is DNS?," 2024. [Online]. Available: [https://www.cloudflare.com/en-gb/learning/dns/what-is-dns/](https://www.cloudflare.com/en-gb/learning/dns/what-is-dns/)

[3] Fortinet, "What is Transmission Control Protocol TCP/IP?," 2024. [Online]. Available: [https://www.fortinet.com/resources/cyberglossary/tcp-ip](https://www.fortinet.com/resources/cyberglossary/tcp-ip)

[4] Mozilla, "Using HTTP cookies," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies)

[5] Mozilla, "Cross-Origin Resource Sharing (CORS)," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)

[6] Mozilla, "Content negotiation," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation)

[7] The New Stack, "Why HTTP Caching matters for APIs," 2023. [Online]. Available: [https://thenewstack.io/why-http-caching-matters-for-apis/](https://thenewstack.io/why-http-caching-matters-for-apis/)

```quiz
Q: In the URL `https://api.example.com/orders/42?status=active`, which part does DNS resolve?
- `api.example.com`
- `/orders/42`
- `?status=active`
- `https`
correct: 0
explain: DNS only resolves the host to an IP address. The path and query are handled after the connection is open; the scheme declares the encryption.

Q: A request works from `curl` but fails in the browser with a CORS error. Why?
- The server is down for browser requests only
- The browser enforces CORS and refused to hand the response to the script; the server likely handled it
correct: 1
explain: CORS is a browser-enforced gate. curl is not a browser, so it ignores CORS. The fix is usually the server's CORS headers, not the client.

Q: A `GET` response carries `ETag: "v3"`. The next request sends `If-None-Match: "v3"` and gets back…
- `200 OK` with a full new body
- `304 Not Modified` with no body — reuse the cached copy
correct: 1
explain: When the ETag matches, the server tells the client its cached copy is still valid via 304, saving the body transfer.

Q: Why is it a problem to put a filter like `?status=active` inside the path instead of the query?
- It identifies the wrong resource and breaks how caches and routers key on the path
- URLs can't contain the word "status"
correct: 0
explain: The path identifies the resource; the query describes filtering. Mixing them confuses caches and routing, which treat the path as primary.

Q: Which HTTP method's responses are conventionally cacheable?
- GET
- POST
correct: 0
explain: GET is safe and idempotent, so caches can store and serve it. Mutating methods like POST are not cached.

Q: TCP/IP underlies every HTTP request. The practical API consequence is…
- TCP handshakes have real latency, so connection reuse and HTTP/2 multiplexing matter for performance
- TCP makes HTTP stateful automatically
correct: 0
explain: Every request opens or reuses a TCP connection; the handshake cost is why we pool connections and multiplex.
```
