There’s a model hiding in your REST API
by Adam Wathan on April 11, 2014REST 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:
id | |
---|---|
1 | john@example.com |
id | title |
---|---|
1 | Gotham News |
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
…
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.
Adam Wathan
Latest posts by Adam Wathan (see all)
- Testing Eloquent Models with Faktory - September 26, 2014
- Using Bootstrap as a Mixin Library - June 6, 2014
- The Virtual Proxy Pattern - May 13, 2014