While working on a backend project recently, I ran into a bug that initially made no sense.
I had business routes that looked something like this:
business := router.Group("/businesses")
// Business Routes
business.Get("/:id", handler.GetBusiness)
// Services
business.Get("/services", handler.GetServices)
business.Get("/services/:id", handler.GetService)
// Products
business.Get("/products", handler.GetProducts)
business.Get("/products/:id", handler.GetProduct)
Everything looked correct.
Or so I thought.
Then something strange happened.
Requests like:
GET /businesses/services
started behaving unexpectedly.
Instead of reaching:
business.Get("/services", handler.GetServices)
the router interpreted:
services
as:
:id
meaning:
/businesses/services
became:
/businesses/:id
with:
id = "services"
At first, I thought something was wrong with my handlers.
The problem was actually route ordering.
Understanding Why This Happens
Routers generally evaluate routes in the order they are registered.
When the router sees:
business.Get("/:id", handler.GetBusiness)
it creates a very flexible matching pattern.
This route says:
"Accept anything after /businesses/ and treat it as an id."
That means:
/businesses/123
matches.
But unfortunately:
/businesses/services
also matches.
And:
/businesses/products
matches too.
Your dynamic route becomes too greedy.
Dynamic Routes Can Swallow Static Routes
Consider this ordering:
business.Get("/:id", handler.GetBusiness)
business.Get("/services", handler.GetServices)
business.Get("/products", handler.GetProducts)
The router reads from top to bottom.
Request:
/businesses/services
Router evaluation:
Step 1:
Does /:id match?
Yes.
Router stops searching.
Static route never executes.
This is sometimes called:
Dynamic Route Swallowing
because dynamic patterns capture requests intended for more specific routes.
The Fix
The simplest fix is:
Register specific routes before dynamic routes.
Instead of:
business.Get("/:id", handler.GetBusiness)
business.Get("/services", handler.GetServices)
Use:
business.Get("/services", handler.GetServices)
business.Get("/products", handler.GetProducts)
business.Get("/:id", handler.GetBusiness)
Now:
/businesses/services
matches:
/services
first.
Only requests that fail earlier matches reach:
/:id
Moving Forward
After debugging this, I learnt a simple rule:
Routes should move from most specific to least specific.
Good:
/services
/services/:id
/products
/products/:id
/:id
Bad:
/:id
/services
/products
Think about dynamic routes as catch-all nets.
The wider the net, the later you should place it.
Lessons Learned
Initially, I thought I had a handler bug.
What I actually had was:
A routing problem.
This bug taught me something important:
Backend bugs are not always business logic bugs. Sometimes the framework is behaving exactly as designed.
Now whenever I add dynamic routes, I immediately ask:
"Can this route accidentally capture requests that belong somewhere else?"
Because eventually:
/:id
tries to become everything.
Have you ever encountered routing bugs like this?
What backend issue took you longer to debug than you expected?
Top comments (0)