Routing is one of the first things every web framework has to solve.
When a request comes in, the framework needs to answer a simple question:
Which piece of code should handle this?
For a long time, one of the most common answers was a central route configuration file.
That approach is still widely used and still works well in many projects. You define URL patterns in one place, map them to controllers or handlers, and the framework reads that configuration to decide what happens next.
But over time, another approach has become increasingly popular:
route metadata declared directly on controllers and handler methods
In PHP, that often means attributes.
Instead of defining routes in a separate file, developers can place route declarations next to the code that actually handles the request.
This article looks at the advantages of that approach, why more frameworks are moving in that direction, and why AssegaiPHP favors route attributes as part of its overall design.
Two valid ways to solve the same problem
Before comparing them, it helps to be clear that both approaches are legitimate.
Traditional route config files
This pattern usually looks something like:
- all routes defined in one or more central files
- paths mapped to handlers
- HTTP methods declared in config
- route groups or prefixes declared externally
The benefit is obvious: all route definitions are collected in one place.
Route attributes
This pattern puts route metadata directly on the controller class and its methods.
In Assegai, that looks like:
<?php
use Assegai\Core\Attributes\Controller;
use Assegai\Core\Attributes\Http\Get;
use Assegai\Core\Attributes\Http\Post;
use Assegai\Core\Attributes\Param;
#[Controller('posts')]
readonly class PostsController
{
public function __construct(private PostsService $postsService)
{
}
#[Get]
public function findAll(): mixed
{
return $this->postsService->findAll();
}
#[Get(':id')]
public function findById(#[Param('id')] int $id): mixed
{
return $this->postsService->findById($id);
}
#[Post]
public function create(): mixed
{
return ['message' => 'Created'];
}
}
That means the route definition sits next to the code that handles it.
The key question is not “Which approach is universally better?”
It is:
Which approach gives the application a clearer and more maintainable structure as it grows?
That is where route attributes start to shine.
1. Route attributes keep related information together
One of the biggest advantages of route attributes is locality.
The route definition lives with the handler.
That means when you open a controller method, you can usually see:
- the HTTP method
- the route path
- the handler name
- parameter bindings
- sometimes guards, interceptors, or metadata
- the code that actually handles the request
That reduces context switching.
With a config file approach, you often have to jump between:
- the routes file
- the controller
- request-binding logic
- middleware or metadata declared elsewhere
That may be fine for a few endpoints.
But in larger applications, keeping related information together tends to reduce friction.
This is one of the strongest arguments for route attributes:
the route is easier to understand because the route and the handler live together
2. They reduce duplication between structure and implementation
A common issue with central route files is duplication.
You might define:
- the path in one place
- the controller method in another
- request binding somewhere else
- validation rules elsewhere
- authorization metadata elsewhere still
None of that is automatically wrong, but it spreads one feature across several files.
Route attributes reduce that spread.
In an attribute-driven controller, the route is part of the same feature surface as:
- the method
- the parameter binding
- the request shape
- the handler's surrounding class/module structure
That tends to make features feel more cohesive.
In practice, this means fewer “where is this defined?” moments.
3. They improve readability at the feature level
When you open a controller with route attributes, the public surface of that feature becomes easy to scan.
For example:
#[Controller('posts')]
readonly class PostsController
{
#[Get]
public function findAll(): mixed {}
#[Get(':id')]
public function findById(#[Param('id')] int $id): mixed {}
#[Post]
public function create(#[Body] CreatePostDTO $dto): mixed {}
#[Patch(':id')]
public function update(#[Param('id')] int $id, #[Body] UpdatePostDTO $dto): mixed {}
#[Delete(':id')]
public function delete(#[Param('id')] int $id): mixed {}
}
Even without reading the method bodies, you can understand the route surface quickly.
That is useful for:
- onboarding
- reviews
- debugging
- documentation
- architectural clarity
The controller becomes a readable map of the feature's HTTP interface.
With traditional route files, that same understanding often requires jumping across files.
4. Attributes make modular applications easier to reason about
AssegaiPHP strongly favors modular application design.
That means features are grouped around things like:
- modules
- controllers
- providers/services
- DTOs
- entities
Route attributes fit naturally into that model because the route becomes part of the feature itself.
Instead of saying:
all routes live over here, all logic lives over there
the framework can say:
this module owns this controller, and this controller owns this route surface
That is especially useful once applications grow beyond a handful of endpoints.
A modular app becomes easier to reason about when each module has:
- its own controllers
- its own services/providers
- its own DTOs
- its own route declarations
That keeps the boundaries clearer.
5. They work well with metadata-driven frameworks
Modern frameworks often rely on metadata for more than routing.
They also use metadata for things like:
- parameter binding
- validation
- guards
- interceptors
- documentation
- serialization behavior
Once a framework is already designed around metadata, route attributes become even more useful because they fit the same mental model.
In Assegai, for example, route attributes do not exist in isolation.
They work alongside:
#[Controller(...)]#[Body]#[Param(...)]- guards
- interceptors
- DTOs
- docs and contract generation
That consistency matters.
It means the framework has one coherent way to express application behavior: metadata close to the code it describes.
6. They support better tooling opportunities
One underrated advantage of route attributes is that they often make tooling easier.
When the framework can inspect controllers, methods, and parameter metadata directly, it becomes easier to derive things like:
- API docs
- OpenAPI metadata
- request contracts
- parameter behavior
- generated clients
That does not mean config-based routing cannot support good tooling.
It can.
But attribute-driven routing often creates a cleaner path because the relevant metadata is already attached to the code structure the framework is inspecting.
This matters more in modern API development than it used to.
If your framework wants to generate docs or clients from the same application structure, route attributes become part of that story.
7. They encourage clearer ownership of endpoints
With central route files, it is possible for routing to feel like a separate layer owned independently from the feature code.
Again, that is not always bad.
But route attributes make ownership clearer.
The controller is not only “the class that might handle something.”
It explicitly declares:
- these are the routes it owns
- this is the path surface it exposes
- these are the methods that respond to specific HTTP verbs
That gives the controller a stronger identity as the feature's HTTP boundary.
In a structured framework like Assegai, that clarity is useful because it aligns nicely with the overall architecture:
- modules define boundaries
- controllers define transport
- providers define behavior
Route attributes strengthen that controller boundary.
8. They make refactoring safer in many cases
Central route files can become difficult to maintain as applications grow.
A rename or move may require updates across several disconnected places:
- controller namespace
- handler name
- route mapping
- middleware assignment
- grouped config
With route attributes, some refactoring becomes more self-contained because the route declaration travels with the handler.
That does not eliminate all refactoring work.
But it often reduces the number of places a developer needs to inspect when changing feature structure.
This becomes especially helpful in teams where features evolve quickly.
9. They make the “happy path” simpler for most apps
One of the strongest practical arguments for route attributes is that they improve the common path.
Most developers are not building highly unusual routing systems every day.
Most of the time, they want to:
- define a controller
- add a route
- bind some params or a body
- call a service
- return a response
Route attributes make that path direct.
The developer does not need to think:
Let me go update the routes file before this handler exists
Instead, the route becomes part of defining the handler itself.
That lowers friction.
And good framework design often comes down to making the common workflow easier without blocking more advanced use cases.
So why do some frameworks still prefer route config files?
Because they also have real strengths.
A central route file can be attractive when a team wants:
- a single global view of all routes
- a strong separation between route declaration and controller code
- explicit route grouping conventions in config
- a style that fits the framework's historical design
There is nothing inherently wrong with that.
In some codebases, especially ones with established conventions, it can work very well.
So the point is not that route config files are “bad.”
The point is that route attributes offer advantages that line up especially well with modern, modular, metadata-driven application design.
Why AssegaiPHP favors route attributes
AssegaiPHP is built around a structured, explicit architecture.
Its core concepts include:
- modules
- controllers
- providers/services
- DTOs
- entities
- attribute-driven metadata
- CLI scaffolding
Given that design, route attributes are the more natural fit.
They support Assegai's broader goals in several ways:
They keep route definitions close to controller behavior
That improves locality and readability.
They fit the framework's metadata model
Assegai already uses attributes heavily for request handling and application structure.
They reinforce modular boundaries
Each controller defines its own route surface inside the feature it belongs to.
They support docs and contract workflows
Because the route is part of inspectable metadata, it works naturally with API docs and generated clients.
They reduce friction for everyday development
A developer can add a route where the handler is actually implemented.
This is not just a stylistic choice.
It is an architectural one.
Assegai favors route attributes because they align with the framework's bigger goal:
make application structure explicit, local, and easier to reason about
Are route attributes always the right answer?
No single design choice is perfect for every team or every framework.
There are cases where centralized routing may still be preferred, especially in ecosystems or codebases built strongly around that style.
But for a framework that values:
- modularity
- metadata-driven behavior
- explicit controllers
- cohesive feature design
- strong tooling around the application structure
route attributes are a very strong choice.
That is why they have gained momentum across modern frameworks and why they make so much sense in AssegaiPHP.
Final thought
The routing question is really a code organization question.
Do you want route information to live in a separate global mapping layer?
Or do you want the route to live next to the code that handles it?
For AssegaiPHP, the answer is clear.
Because the framework is designed around explicit structure, modular features, and metadata that works together, route attributes help the application stay easier to read, easier to maintain, and easier to extend.
That is the real advantage.
Not novelty. Not trend-chasing.
Just a better fit for the kind of architecture Assegai is built to support.