Docs Custom Router

Custom Router

Plumego’s built-in router is intentionally simple, explicit, and boring.

For the vast majority of systems, it is more than sufficient.

However, in a small number of advanced scenarios, you may need to introduce
custom routing behavior — without breaking Plumego’s core guarantees.

This document explains:

  • When a custom router is justified
  • When it is absolutely not
  • How to introduce one safely
  • What constraints must never be violated

First Principle: The Default Router Is the Baseline

Before considering a custom router, internalize this rule:

If you cannot clearly explain why the default router is insufficient, you should not replace or extend it.

The default router already provides:

  • Deterministic method + path matching
  • Explicit registration
  • Clear middleware scoping
  • Predictable dispatch

Most “router problems” are actually architecture or middleware problems.


Legitimate Reasons for a Custom Router

A custom router may be justified if all of the following are true:

1. The requirement is structural, not stylistic

Valid examples:

  • Extremely high-cardinality dynamic routing (e.g. multi-tenant host-based routing)
  • Protocol multiplexing on a single port
  • API gateway–style dispatch based on headers or subprotocols
  • Specialized performance constraints proven by profiling

Invalid examples:

  • “I prefer a different syntax”
  • “This other framework does it differently”
  • “I want auto-generated routes”

2. The behavior cannot be expressed via middleware + handlers

If middleware and handlers can solve the problem cleanly,
introducing a custom router is unnecessary.

Routing is about selection, not decision logic.


3. The routing decision must happen before handler selection

If the decision can happen inside a handler,
you do not need a custom router.


What a Custom Router Must Not Do

Any custom routing logic must preserve Plumego’s core guarantees.

It must not:

  • Add hidden lifecycle phases
  • Introduce implicit middleware
  • Perform authentication or authorization
  • Execute business logic
  • Create multiple handlers for one request
  • Retry or fork request execution

If any of these happen, the router is no longer a router.


Supported Extension Model

Plumego does not support pluggable router engines via magic hooks.

Instead, the supported model is:

Wrap or replace routing logic explicitly at the HTTP handler boundary.

This keeps behavior visible and testable.


Pattern 1: Router as a Pre-Dispatcher

In this pattern, you introduce a pre-router that decides
which Plumego app or handler should receive the request.

Example use cases:

  • Host-based routing
  • Protocol negotiation
  • Multi-app composition

Conceptual example:

type MultiAppRouter struct {
	apps map[string]http.Handler
}

func (r *MultiAppRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	host := req.Host

	if app, ok := r.apps[host]; ok {
		app.ServeHTTP(w, req)
		return
	}

	http.NotFound(w, req)
}

Each app can be a fully independent Plumego instance.

Plumego’s internal lifecycle remains untouched.


Pattern 2: Router as a Header-Based Dispatcher

In some advanced API gateway scenarios,
routing decisions depend on headers rather than paths.

Example:

  • X-API-Version
  • X-Tenant-ID
  • Subprotocol negotiation

You can implement this outside Plumego:

func VersionRouter(v1, v2 http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.Header.Get("X-API-Version") {
		case "v2":
			v2.ServeHTTP(w, r)
		default:
			v1.ServeHTTP(w, r)
		}
	})
}

Each handler may be a Plumego app with its own routing table.


Pattern 3: Internal Router Replacement (Rare)

In very rare cases, you may want to replace Plumego’s router internally.

This should only happen if:

  • You fully control the Plumego fork
  • You are willing to maintain it
  • You have strict performance requirements
  • Profiling proves routing is the bottleneck

Even then, your replacement must:

  • Preserve method + path semantics
  • Preserve deterministic matching
  • Preserve middleware ordering
  • Preserve lifecycle guarantees

This is advanced framework work, not application code.


What Not to Do

Do Not Encode Business Logic in Routing

Routing selects where execution goes,
not what happens.

If routing decisions depend on domain rules,
those rules belong in usecases.


Do Not Create “Smart” Routers

Routers that:

  • Validate input
  • Authorize users
  • Transform payloads

Become untestable and unpredictable.


Do Not Mix Routing and Middleware Concerns

Middleware wraps execution.
Routing selects execution.

Blurring this line leads to hidden behavior.


Testing Custom Routing

Custom routing must be tested independently.

Recommended tests:

  • Routing decision tests (input → selected handler)
  • Fallback behavior
  • No-route behavior
  • Performance benchmarks (if performance is the reason)

Do not rely on integration tests alone.


Maintenance Cost Warning

Every custom router introduces:

  • Additional cognitive load
  • Additional maintenance burden
  • Upgrade friction
  • Onboarding complexity

Before introducing one, ask:

“Is this complexity truly unavoidable?”

Most of the time, the answer is no.


Summary

In Plumego:

  • The default router is intentionally sufficient
  • Custom routers are an advanced, rare tool
  • Routing must remain selection-only
  • Lifecycle guarantees must be preserved
  • Explicitness always wins over cleverness

If you need a custom router, build it around Plumego,
not inside it — unless you are prepared to own the consequences.


Next

A natural continuation after routing customization is:

→ Advanced / Performance Considerations

This explains how to reason about throughput, latency, and allocation
without sacrificing clarity or correctness.