Generally, we use the same conventional pattern for our Web and API routes for our models. Let’s see, how to create a reusable trait that generates the model’s route attributes and how to append them in the JSON or array form of it.

The URL Scheme

As a level zero, we can agree on using the RESTful URI scheme. That means every model has an associated route. We can perform different actions on the model, depending on the request type. Let’s see some examples:

// View
GET /posts/{id}

// Update
PATCH /posts/{id}

// Delete
DELETE /posts/{id}

It can be a bit painful to concatenate the strings and IDs all the time, so we could append this URL as an attribute of the model. That means we could calculate the URL behind the scenes, and use it on the PHP or the JS side as well. For example some usage:

// PHP
$post->web_route // /posts/1

// JS
post.api_route // /api/posts/1

Then only difference (in this simple example) between the API and the web routes is that the API routes have a /api prefix in the URL.

The Routable Trait

First, let’s clear why do we use a trait here. Since we may use the same pattern for our models, we should extract the logic in a reusable concern, which is a trait in our case. Move on and compose the trait itself:

The API route is not more than the web route with a prefix, so we should start by generating the web route first. The base formula is very easy: the pluralized name of the model + the model ID.

// app/Concerns/Routable.php

<?php

namespace App\Concerns;

use ReflectionClass;
trait Routable
{
    /**
     * Get the api route.
     *
     * @return string
     */
    public function getApiRouteAttribute()
    {
        return $this->id ? url('api', $this->baseRoute()) : null;
    }

    /**
     * Get the web route.
     *
     * @return string
     */
    public function getWebRouteAttribute()
    {
        return $this->id ? url($this->baseRoute()) : null;
    }

    /**
     * Get the base route.
     *
     * @return string
     */
    protected function baseRoute()
    {
        return sprintf('%s/%s',
            strtolower(str_plural((new ReflectionClass($this))->getShortName())),
            $this->id
        );
    }
}

So, what’s going on here? First of all, we calculate the base route. It consists of the pluralized name of the model and its ID. For example products/2.

Then we generate both the web and the API endpoints for the model. By using the url() helper, we Laravel appends the applications’ URL for the string. If we pass more parameters, then they will be imploded by a /.

Note, we check if the model has an ID or not. Because, if we just create a new instance of a model, that is not stored in the database, we don’t need any API or web routes to be appended. Also, it can blow up our tests, so we return null.

Appending the Attributes to the Model’s JSON/Array Form

We can use these computed attributes from PHP now, but if we want to use them in JS, for example, we need to append these to the JSON or array form of the model. Fortunately, we easily can do this with the $appends eloquent property.

<?php

namespace App;

use App\Concerns\Routable;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use Routable;

    /**
     * The accessors to append to the model's array form.
     *
     * @var array
     */
    protected $appends = [
        'api_route',
        'web_route',
    ];

    // ...
}
Don’t forget to import and use the Routable trait in your model!

Summary

It’s a more clean approach to use a model’s endpoint than to concatenating strings and IDs in PHP or JS. Of course, you may customize the logic inside the route generators, but it can be a nice starting point.