Routing Patterns — URL Routing in Backend Services

Status: Active | Last Updated: 2026-08-29 Category: Backend — Routing Prerequisites: HTTP basics Tags: routing, url, methods, route-groups, params Estimated Time: 3-4 hours (Self-paced, includes lab time)

Summary

Routing maps an incoming request to a handler. Static paths (/about) are easy. Parameterized paths (/users/:id) need parsing. Wildcards, query strings, and method matching all need to be considered. This article covers the patterns used in modern backends, from simple if/else chains to structured route groups.

What You'll Learn (Core Competencies)

Table of Contents

  1. Architectural Overview & Core Schema
  2. Deep Dive & Implementation
  3. Anti-Patterns & Common Pitfalls
  4. Independent Challenge
  5. Consolidation & Key Invariants
  6. Next Steps

1. Architectural Overview & Core Schema

Routing is the interface between clients and business logic. A well-designed route structure communicates intent: /v1/users/42 tells a developer this is a versioned user resource, while /users/42/posts shows a nested relationship. Poor routing leads to ambiguous URLs, duplicated handlers, and brittle code.

Performance considerations include how quickly the server can resolve a path to a handler. A flat array of exact-match routes is faster than a nested regex chain. For high-traffic APIs, prefer static lookup tables or trie-based routers over dynamic regex evaluation for every request.

Security is also tied to routing: routes define the surface area of your API. Unused routes should not exist; route-level middleware applies authentication only where needed; and parameterized routes must sanitize inputs to prevent injection attacks.

There are three fundamental route types:

Static routes are the easiest to cache and the fastest to resolve. Parameterized routes require parsing the path segments. Wildcards are useful for file serving or legacy URL compatibility but make authentication and rate-limiting harder because the path is unpredictable.

2. Deep Dive & Implementation

Static and Parameterized Routes

Extract path segments by splitting the pathname:

const url = new URL(request.url);
const parts = url.pathname.split("/");
// /users/42 -> ["", "users", "42"]
const userId = parts[2];

For parameterized routes, validate the extracted parameter before using it. An id extracted from the path should be checked against the expected type (integer, UUID, slug).

Method Routing

HTTP methods map to CRUD operations:

Method Purpose Idempotent
GET Read Yes
POST Create No
PUT Replace Yes
PATCH Partial update No
DELETE Remove Yes

Idempotent means calling it multiple times has the same effect as calling once. PUT and DELETE are idempotent; POST is not (each call creates another resource). This matters for retry safety on unreliable networks.

In Bun, combine path and method checks:

Bun.serve({
  port: 3000,
  fetch(request) {
    const url = new URL(request.url);
    const path = url.pathname;

    if (path === "/users" && request.method === "GET") {
      return Response.json({ users: [] });
    }
    if (path === "/users" && request.method === "POST") {
      return Response.json({ created: true }, { status: 201 });
    }
    if (path.startsWith("/users/") && request.method === "GET") {
      const id = path.split("/")[2];
      return Response.json({ user: { id } });
    }
    return new Response("Not Found", { status: 404 });
  }
});

Always check the method explicitly. A GET handler that writes to a database violates HTTP semantics and can cause unexpected behavior with caching proxies and browsers.

Query Strings

Query parameters come after ? in the URL:

GET /search?q=fogserv&limit=10&page=2

Parse them with URLSearchParams:

const url = new URL(request.url);
const q = url.searchParams.get("q");
const limit = parseInt(url.searchParams.get("limit") ?? "10", 10);
const page = parseInt(url.searchParams.get("page") ?? "1", 10);

Query strings are for optional filtering, sorting, and pagination — not for resource identification. The resource identity should always come from the path (/users/42), not the query (?id=42). This separation makes URLs predictable and cache-friendly.

Always validate query parameters. Unvalidated limit values can cause denial of service if a user requests limit=100000. Cap pagination limits and sanitize inputs before passing them to databases.

Route Groups and Prefixes

As APIs grow, you need groups: a prefix applied to multiple routes, shared middleware, or versioned paths. Common patterns include version prefixes (/v1/, /v2/) and feature groups (/admin/, /public/).

In raw Bun.serve(), implement groups by checking the prefix before specific routes:

Bun.serve({
  port: 3000,
  async fetch(request) {
    const url = new URL(request.url);
    const path = url.pathname;

    // Admin group — requires auth check
    if (path.startsWith("/admin/")) {
      return handleAdmin(request, url);
    }

    // Public group — CORS enabled
    if (path.startsWith("/public/")) {
      return handlePublic(request, url);
    }

    return new Response("Not Found", { status: 404 });
  }
});

Using a router library, this becomes a first-class feature. Route groups reduce duplication: apply CORS to /public/ but require auth for /admin/. Without groups, every route repeats the same checks.

Guided Checkpoint

Test the router with a variety of requests:

# Start: bun run server.ts

# Static route — GET returns 200
curl -s -w "\n%{http_code}" http://localhost:3000/users

# Wrong method — GET on POST-only route returns 404 (no handler)
curl -s -w "\n%{http_code}" -X DELETE http://localhost:3000/users

# Parameter extraction — :id parsed from path
curl -s http://localhost:3000/users/99 | jq .

# Nested parameterized route
curl -s http://localhost:3000/posts/5/comments/12 | jq .

# Query string — filtering via URLSearchParams
curl -s "http://localhost:3000/search?q=bun&limit=5" | jq .

# Pagination — offset and limit
curl -s "http://localhost:3000/users?page=2&limit=10" | jq .

# Wildcard — catch-all serves static content
curl -s http://localhost:3000/static/js/app.js | head -c 100

# Unmatched path — returns 404
curl -s -w "\n%{http_code}" http://localhost:3000/nonexistent/path

# Versioned group
curl -s http://localhost:3000/v1/status | jq .

The router should return the correct status code and response body for each case. Unmatched paths must return 404, not 200.

3. Anti-Patterns & Common Pitfalls

Overusing wildcards. Wildcards (/files/*) make URL matching unpredictable and break caching because proxies cannot know what resources will be served. Prefer parameterized routes (/files/:path) for variable segments.

Ignoring HTTP methods. A path like /users should behave differently under GET vs POST. Always branch on request.method. A GET handler that writes to a database violates HTTP semantics and will cause issues with caching proxies and browser preloading.

Query parameters for resource identity. Do not put resource IDs in query strings when a path parameter works. Cache and bookmark behavior differ: GET /users/42 is cacheable and bookmarkable; GET /users?id=42 is not.

No 404 handling. Every router needs a fallback. Return 404 for unmatched paths, not 200 with an empty body. Clients relying on status codes will treat an empty 200 as a successful response.

Regex without bounds. Complex regex routes are slow and hard to maintain. A simple pathname.startsWith("/users/") check is faster and more readable than /^\/users\/[a-z0-9-]+$/i.

Unvalidated path parameters. Extracting :id from /users/abc and passing it to a SQL query without validation causes injection vulnerabilities. Always validate extracted parameters against the expected format (integer, UUID, etc.) before use.

Not returning 405 Method Not Allowed for supported paths with wrong methods. If /users supports GET and POST but not DELETE, returning 404 for DELETE is misleading. The path exists — the method does not. Return 405 with an Allow header listing supported methods.

4. Independent Challenge

Design a versioned API with nested resources that handles the following routes:

GET    /v1/posts              → list posts (paginated: ?page, ?limit)
POST   /v1/posts              → create a post
GET    /v1/posts/:postId      → read a single post
DELETE /v1/posts/:postId      → delete a post
GET    /v1/posts/:postId/comments → list comments on a post
POST   /v1/posts/:postId/comments → add a comment
GET    /v1/users/:userId      → read a user profile

Implement the router in Bun without a router library. Return realistic JSON responses (use an in-memory array as a fake database). Handle query string pagination for the list endpoints. Return 404 for unmatched paths and 405 for unsupported methods on matched paths. Include an X-Api-Version: v1 response header on all routes.

5. Consolidation & Key Invariants

6. Next Steps


Change Log

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse