API Versioning in Laravel: The Complete Guide to Doing it Right

Navigate Laravel's API versioning seamlessly with URI Versioning, ensuring consistent, reliable integration even as your application evolves.

6 months ago   •   5 min read

By Steve McDougall
Table of contents


Ensuring your API remains consistent and reliable is akin to keeping a ship steady during a storm. As your application grows and changes, so too does the need to manage different versions of your API. Laravel, a popular PHP framework, offers a plethora of tools to help you navigate these choppy waters. But with so many routes to take, how do you ensure you're on the right path? Enter API versioning.


Simplify managing API versions with Treblle: the tool that ensures consistency and reliability as your application evolves.

Start with Treblle today!

You can version your Laravel API in many ways, but the most straightforward approach is using URI Versioning. This is where your URI path will include a /v*/ where * is the version number. You wouldn't typically go full into semantic versioning with your API versioning here, as that can cause painful problems for those integrating with your API. Instead, you focus on the major version constraint and try to keep breaking changes in those versions as far away as possible.


Streamline your API versioning process with Treblle, making implementation easier and more efficient.

Discover how with Treblle!

Typically, your Laravel application will have a few route files in the routes directory of your application. The main ones we care about here are web.php and api.php, where we register all our Web and API routes for Laravel. This is loaded in through app/Providers/RouteServiceProvider.php, at least when writing, where Laravel 10 is the main version we consider. When Laravel 11 is released, with its new lightweight skeleton, we may have to rethink certain things.

Inside the RouteServiceProvider, we have the following code, which loads in the route files for the application.

public function boot(): void  
{  
	// Rate Limiter 
  
	$this->routes(function (): void {  
		Route::middleware('api')->prefix('api')->group(  
			base_path('routes/api.php'),  
		);  
  
		Route::middleware('web')->group(  
			base_path('routes/web.php'),  
		);  
	});  
}  

This tells our Laravel application to load both the web and API routes, one with a prefix of api and the other without. It also specifies which middleware groups should be followed for the routes being loaded.

If you are building an application with both Web and API interfaces available, keeping this as it is - this is absolutely fine. If you remove the web interface and stick with an API application, remove the prefix and the web route definition, as you won't need it.

The Secret Sauce to Laravel API Versioning

What I like to do if I am using both a web and an API interface for my application - is to create additional directories in the routes directory - so I end up with the following setup:

  • routes/web/routes.php
  • routes/api/routes.php

This allows me to keep them completely separated, and I can split out my route grouping as much as I like without worrying about file naming conflicts across the web and API route files.


Enhance your versioning strategies with Treblle. Gain insights into security and efficiency for your APIs.

Enhance now with Treblle!

Let's take an example of an e-commerce store that sells company swag. If this were an API-only platform, we would have routes such as products or orders and categories so that people can search, refine, and order what they may be looking for. We could keep all of this in one central routes.php file. However, as your application gets bigger, managing a large file like this gets harder and harder. Let's look at an example of this, especially if you are also going to implement versioning within your API.

Route::prefix('v1')->as('v1:')->group(static function (): void {
	Route::prefix('orders')->as('orders:')->group(static function (): void {
		Route::get('/', Orders\V1\ListHandler::class)->name('list');
	});

	Route::prefix('products')->as('products:')->group(static function (): void {
		Route::get('/', Products\V1\ListHandler::class)->name('list');
	});
});

Route::prefix('v2')->as('v2:')->group(static function (): void {
	Route::prefix('orders')->as('orders:')->group(static function (): void {
		Route::get('/', Orders\V2\ListHandler::class)->name('list');
	});

	Route::prefix('products')->as('products:')->group(static function (): void {
		Route::get('/', Products\V2\ListHandler::class)->name('list');
	});
});

That is only two endpoints, both relatively straightforward. However, visually, it can be a lot to take in. When building your Laravel application, we want to consider the cognitive load of working on this application. If you have to hunt and search for a simple route definition in your application, how will you be by the time you get to the actual code you need to work on?

What I like to do here is structure that main entry route file in the following way:

Route::prefix('v1')->as('v1:')->group(
	base_path('routes/v1/routes.php'),
);

Route::prefix('v2')->as('v2:')->group(
	base_path('routes/v2/routes.php'),
);

This makes the entry point nice and straightforward, just pointing to a different location. If you care about v1 routes, check that file. It is predictable. You know, as soon as you open the routes directory with one file and directories named after API versions, you immediately know where to look. You can make it easy to navigate from here, reducing the cognitive load of managing the application.


Reduce the cognitive load and streamline managing your application with Treblle.

Streamline with Treblle now!

Let's look inside routes/v1/routes.php to understand how we might split this up.

Route::prefix('orders')->as('orders:')->group(
	base_path('routes/v1/orders.php'),
);

Route::prefix('products')->as('orders:')->group(
	base_path('routes/v1/products.php'),
);

We are splitting the routes out further by separating the grouped routes into individual files, reducing the cognitive load even further. So that when we open routes/v1, we can see all the route groups within our application at a glance and go directly to the group that we know we need to look at. Furthermore, this does not affect your application's performance by including multiple files as you would typically cache your routes in production anyway - which will combine all of the routes into one easily readable file by the framework.

To mirror this, our controllers or handlers are also managed within corresponding namespaces, allowing easy traversal within your application. I have seen people who will override the RouteServiceProvider for route groups and loading. I used to be one of those people, too. However, I found it unintuitive to look at the route service provider to understand the grouping compared to going directly to the route files themselves.

Versioning your Laravel API is easy, but it takes careful planning and consideration. It would be best to think about the API you will have tomorrow instead of the requirements you must meet today.

💡
Implement and manage your API versioning effectively with Treblle as your partner. Start effectively with Treblle!

Spread the word

Keep reading