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.