Search code examples
laravelapiversioning

A different way to version api output with laravel?


I'm about to ...

  1. extend my App/Orm/MyModel.php with Http/Json/V1/MyModel.php so I can keep the $appends, $hides, toArray() neatly tucked away in a V1
  2. namespace and prefix some routing for V1
  3. probably do some custom resolvers for route model binding

And I'm thinking ... really? They haven't built this in... what am I missing here? There's gotta be a quick, turn-key for this. I'm interested in knowing how other people are doing this, so please chime in.


Solution

  • Try Resources instead of Models

    Have a look at resources: https://laravel.com/docs/5.7/eloquent-resources

    And add your logic to resources so that you display different versions of a model depending on the API version. You can still make use of $appends and $hidden.

    With this approach we return a Resource of a model rather than the model itself.

    Here is an example of a UserResource for different API versions:

    class UserResource extends JsonResource
    {
        private $apiVersion;
    
        public function __construct($resource, int $apiVersion = 2) {
            $this->apiVersion = $apiVersion; // OPTION 1: Pass API version in the constructor
            parent::__construct($resource);
        }
    
        public function toArray($request): array
        {
            // OPTION 2: Get API version in the request (ideally header)
            // $apiVersion = $request->header('x-api-version', 2);
    
            /** @var User $user */
            $user = $this->resource;
    
            return [
                'type' => 'user',
                'id' => $user->id,
                $this->mergeWhen($this->apiVersion < 2, [
                    'name' => "{$user->first_name} {$user->last_name}",
                ], [
                    'name' => [
                        'first' => $user->first_name,
                        'last' => $user->last_name
                    ],
                ]),
                'score' => $user->score,
            ];
        }
    }
    

    The you can call:

    $user = User::find(5);
    return new UserResource($user);
    

    If you need a different connection you can do:

    $user = User::on('second_db_connection')->find(5);
    

    So V1 API gets:

    {
        id: 5,
        name: "John Smith",
        score: 5
    }
    

    and V2 API gets:

    {
        id: 5,
        name: {
             first: "John",
             last: "Smith",
        },
        score: 5
    }
    

    Now if later you wanted to rename score to points in your DB, and in V3 of your API you also wanted to change your JSON output, but maintain backwards compatibility you can do:

       $this->mergeWhen($this->apiVersion < 3, [
           'score' => $user->points,    
       ], [
           'points' => $user->points,
       ])
    

    Prefix routes

    You can easily prefix routes as mentioned here: https://laravel.com/docs/5.7/routing#route-group-prefixes

    Route::prefix('v1')->group(function () {
        Route::get('users', function () {
            // ...
        });
    });
    

    Explicit Route Model Binding

    To do custom route model bindings have a look at: https://laravel.com/docs/5.7/routing#route-model-binding

    e.g.

    Route::bind('user', function ($value) {
        return App\User::where('name', $value)->first() ?? abort(404); // your customer logic
    });