There’s a model hiding in your REST API

by Adam Wathan on April 11, 2014

REST is a great standard, but sometimes you need an endpoint that breaks convention.

Say your application manages mailing lists, and the users subscribed to them. Your database structure might look like this:

users
id email
1 john@example.com

mailing_lists
id title
1 Gotham News

mailing_list_users
id user_id mailing_list_id
1 1 1
1 1 2

…and you’ve got a couple of related models:

class User
{
    public function mailingLists()
    {
        return $this->belongsToMany('MailingList');
    }
}

class MailingList
{
    public function users()
    {
        return $this->belongsToMany('User');
    }
}

Seems straightforward enough right? So you create a couple of resources:

Route::resource('users', 'UsersContoller');
Route::resource('mailing-lists', 'MailingListsContoller');

…which works great, until you need to be able to subscribe a user to a mailing list.

So you start thinking…

“Hmm… the action I’m trying to do is subscribe, that’s not really create, or update…”

“I guess maybe a POST request to /mailing-lists/1/subscribe, isn’t really that bad?”

And at first it really doesn’t seem so bad, I mean what else can you do? You aren’t really updating the list, and you’re certainly not creating, deleting, or retrieving the list.

But by settling for this, you’re actually missing a great opportunity to enrich your domain. When you can’t fit an action you need to perform into a REST verb, it’s often because you have a concept in your system that you haven’t named yet.

So if we force ourselves to work within the constraints of create/read/update/delete, what is it that we are creating, reading, updating or deleting?

In the case of a many-to-many relationship like this one, one trick I like to use is to force myself to describe it as just a has-many relationship.

Saying a user has-many mailing lists doesn’t really make sense. They don’t own the mailing list.

But if a user subscribes to many mailing lists, you could say that the user has-many subscriptions. And when a user is subscribing to a mailing list, they are creating a new subscription.

Wait a minute, create! That’s a REST verb!

So something like a POST request to /subscriptions sounds just about perfect.

Extracting a new resource

You could create a new route just for subscribing and piggyback off of another controller:

Route::resource('users', 'UsersContoller');
Route::resource('mailing-lists', 'MailingListsContoller');
Route::post('subscriptions', 'MailingListsContoller@subscribe');

Or you could take this opportunity to add a whole new resource!

Route::resource('users', 'UsersContoller');
Route::resource('mailing-lists', 'MailingListsContoller');
Route::resource('subscriptions', 'SubscriptionsContoller');

“Huh?! But I don’t have a Subscription model?”

Ah, but you do!

Remember that mailing_list_users table? How about we rename that to subscriptions

subscriptions
id user_id mailing_list_id
1 1 1
1 1 2

We can create that Subscription model now, and update some of our relationships with some better names…

class Subscription
{
    public function user()
    {
        return $this->belongsTo('User');
    }

    public function mailingList()
    {
        return $this->belongsTo('MailingList');
    }
}

class User
{
    public function subscribedMailingLists()
    {
        return $this->belongsToMany('MailingList', 'subscriptions');
    }

    public function subscriptions()
    {
        return $this->hasMany('Subscription');
    }
}

class MailingList
{
    public function subscribers()
    {
        return $this->belongsToMany('User', 'subscriptions');
    }
}

Now if we ever need to add any extra metadata to a subscription, we have a nice way to work with it, rather than trying to do a bunch of quirky stuff on a pivot table that has no representation in our system.

Maybe we need to store a last_payment_received_at timestamp on a subscription.

With our original setup, first we would’ve had to update the relationship:

class User
{
    public function mailingLists()
    {
        return $this->belongsToMany('MailingList')->withPivot('last_payment_received_at');
    }
}

And then to use that information, we’d have to access it through the pivot property on a user’s mailing lists:

foreach ($user->mailingLists as $mailingList) {
    echo $mailingList->pivot->last_payment_received_at;
}

This really doesn’t feel like we are modeling the domain cleanly anymore, does it?

Compare that to how it would look with our Subscription model in place:

foreach ($user->subscriptions as $subscription) {
    echo $subscription->last_payment_received_at;
}

Much clearer, right?

So next time you find yourself needing to break away from REST conventions, take a step back and force yourself to work within the constraints REST outlines. There’s a good chance your design will be better because of it.

The following two tabs change content below.

Adam Wathan

Developer, powerlifter and resident Slayer fan.

Latest posts by Adam Wathan (see all)