Motivation for API resources0:00
Look, if you've got a moment, I'd like to show you why I want to create API resources in the first place. So, if you're not interested in this part, feel free to skip ahead. I think it might be useful for a few of you, though, so you understand why I'm doing this now, rather than later down the line. In order to demo this, let's go to our routes file, and I'm going to create a little debug route we can make use of, just for a moment. So, let's go to the test endpoint, and we'll open up an inline closure, because we're going to delete this momentarily, and I'm just going to grab the first User in the database. So, User::find(1), and I'll return that. So, Laravel is going to convert this into JSON for us to view in the browser. So, from the browser, we'll go to /test, and sure enough, here is the first User, Ronnie Becker, inside our database. Now, already, there are a few
to view in the browser. So, from the browser, we'll go to /test, and sure enough, here is the first User, Ronnie Becker, inside our database. Now, already, there are a few columns here that I'm questioning whether we should be able to see, certainly as a guest, but even as an authenticated user. Why should we know when Ronnie verified his email? Why should we know if Ronnie has confirmed two-factor authentication? We probably wouldn't be able to use that information in some form of security leak, but it's still giving perhaps a little more information than we should. So, we need a way to control what the end user is able to see when Laravel converts a model into JSON. Now, there are a couple of ways to do this. Let me show you the simplest way, and then we'll talk about why that's not suitable for our particular needs. Let's jump into the User model, and you can see here on line
Using hidden and visible1:30
this. Let me show you the simplest way, and then we'll talk about why that's not suitable for our particular needs. Let's jump into the User model, and you can see here on line 38, in our case, we have an array called hidden, and it's filled with columns that we want to hide from the front end. So, any time a User is converted to JSON, Laravel will take a look at this array, and it will say, yep, get rid of the password, get rid of the remember token, get rid of two-factor recovery codes, and two-factor secret. And you can see that's the case, because those columns don't appear in the output here. We could add, let's say updated_at. So, here's updated_at, and we'll come back and refresh, and now there is no updated_at column in the output. We only see created_at. We could also do the opposite. So, rather than having a deny list, we could have an allow list, which is perhaps a little
updated at column in the output. We only see created_at. We could also do the opposite. So, rather than having a deny list, we could have an allow list, which is perhaps a little more strict. So, we could say visible, and visible is an array, and this array is again an array of columns. So, let's say we want to show, we want to make visible the ID, perhaps the name, maybe the email, and let's have the updated_at column as well. Refresh, and now the only columns that are converted to JSON are the ones we've specified explicitly in that visible array. So, why don't we just use this? Why are we going to reach for API resources? Well, here's the deal. You cannot pick and choose which columns to display by using hidden and visible. Let me expand on that. Let's say we are Ronnie Becker. Well, if we are Ronnie Becker, there's nothing wrong with us seeing our own email address. We probably
using hidden and visible. Let me expand on that. Let's say we are Ronnie Becker. Well, if we are Ronnie Becker, there's nothing wrong with us seeing our own email address. We probably need to have access to our own email address to display it in various parts of the application. But if we aren't Ronnie Becker, we probably don't want to see the email address of every single user, and that's especially the case if we're actually dealing with a guest user, because otherwise, well, it would be easy to create a scraper that could just pull everybody's email addresses from a page of forum posts. It would just loop through, grab all of the user data and scrape those emails, and we've just allowed that by not hiding the email address properly for a given user. So, that's a very small security leak, but you can see how this can bubble up, and you could actually be leaking some very personal and private.
Generating API resources3:49
address properly for a given User. So, that's a very small security leak, but you can see how this can bubble up, and you could actually be leaking some very personal and private data accidentally. API resources allow us the extra control that we'll need to make sure that that cannot and does not happen. This is also the sort of stuff that's very difficult to test for in an automated test. So, by thinking of it upfront, we're going to save ourselves a lot of time. Hopefully, that explanation makes sense, and you're happy with why we're going to reach for API resources in the case of this particular application. If you're on board, let's go ahead and create three API resources, one for our User, one for our Posts, and one for our Comments. So, we'll say php artisan make:resource, and we want a resource for the User first, so UserResource will do, and now in the IDE, we should be
for our posts, and one for our comments. So, we'll say php artisan make:resource, and we want a resource for the User first, so UserResource will do, and now in the IDE, we should be able to open up our newly created UserResource. Now, we'll have one method, toArray, and this is where we define that transformation between the model and the JSON that will be output. Now, by default, it's going to do the exact same as what would happen if you had a hidden or visible array on your model, but we can return any array from here. So, let's go ahead and convert this to an array, and we'll start with just a single property, id, right? So, this id, and you can access any of the resource's properties directly, even though we're on the UserResource class. It uses magic access to give you access to the underlying resource. Now, we'll stop there. How do we actually make use of this from the backend, from a
Applying UserResource output5:17
the UserResource class. It uses magic accesses to give you access to the underlying resource. Now, we'll stop there. How do we actually make use of this from the backend, from a controller? Let's go back to our web.php file, and I'm going to wrap userFind in a UserResource. So, UserResource, there's a static make method, and I'm going to wrap userFind inside that make method. All right, let's go back to the browser and refresh, and sure enough, we now have JSON that contains a property called data, and then an object with our user's id. Note that we have this additional data property, by the way. This is because it's an API resource, so it will wrap it in data so that we can specify additional top-level data around it. If you don't want that, we can turn that off, and in our case, because we're not going to be providing a public API, I will turn it off. I'll jump into the AppServiceProvider,
If you don't want that, we can turn that off, and in our case, because we're not going to be providing a public API, I will turn it off. I'll jump into the AppServiceProvider, and then down here in the boot method, we can call the JsonResource class, which is the base class for all API resources, and without wrapping. And now, from the browser, when we refresh, we have something very similar to what we had previously. We just show the columns that we've allowed inside the resource itself. Okay, let's go back into our UserResource, and let's expand on the UserResource itself. So, yeah, maybe we output the name as well. We also want to output the email address, but again, as we've mentioned, we only want to show this if you're currently authenticated as the User you are viewing, and JSON resources have a cool method called when that we can use for this exact purpose. So, we could say
to show this if you're currently authenticated as the User you are viewing, and JSON resources have a cool method called when that we can use for this exact purpose. So, we could say only when this ID is equal to the request user ID, okay? In other words, are you the User that you are viewing? If you are, we'll show this email like so. Okay, so if I go back to the test route, attempt to read property ID on null. What have we done wrong there? Ah, we're going to have to use a conditional here because there may not be a User actually signed into the request, as is the case here. But as you can see, we're not logged in, so we don't see the email address. But if we go ahead and log in, so I'm going to go to the login route, and let's log in as test@example.com with our password, head back to the /test route. We're not Ronnie Becker, so we don't see that, but if
the login route, and let's log in as test at example.com with our password, head back to the forward slash test route. We're not Ronnie Becker, so we don't see that, but if we were to go to web.php and instead find User 11, which is our user, and refresh, well now we see our email address. So, can you see the power of JSON resources here in showing or hiding that information correctly? Super useful for these kind of scenarios. All right, what's next? Well, we could add the updated_at and created_at columns. So, this updated_at, and of course, we'll duplicate that, and we're looking for created_at. And those should show nicely in the pretty print, which they do. Finally, we probably want to add that profile_photo_url, so let's do that. profile_photo_url is equal to this->profile_photo_url. And I think that's all we'll need for now for the User. Obviously, we can expand
Creating Post and Comment resources8:25
that profilePhotoUrl, so let's do that. profilePhotoUrl is equal to this profilePhotoUrl. And I think that's all we'll need for now for the User. Obviously, we can expand on that as time goes on. All right, why don't we go ahead and create a resource for Post now? So, back in our terminal, php artisan make:resource, and this is the PostResource. At the same time, let's go ahead and say make:resource once more, and we'll create the CommentResource so that we don't have to come back to the terminal each and every time. I'm also going to jump into the test endpoint that we have, and I'm going to update it to show all three resources that we're creating so that I don't have to keep going back and forward and making changes. So, we obviously want a PostResource, and that's going to wrap a Post. So, let's say Post::find(1), and then we want the same thing with a CommentResource,
and making changes. So, we obviously want a Post resource, and that's going to wrap a Post. So, let's say Post::find(1), and then we want the same thing with a Comment resource, right? So, Comment resource, and we can wrap the first Comment in the database. So, Comment::find(1) as well. And yeah, here we go. That's what it looks like. So, this is our User. This is the current output of a Post, and this is the current output of a Comment. So, let's work on the Post next, and in all honesty, these columns are absolutely fine apart from the user_id. Let's go to the Post resource, down to parentToArray, and we can say ID is this ID. We can say title is this title. We have the body, right? So, body is this body. We may change the body down the line, but it's fine for now. We have updated_at, this updated_at, and created_at, which is going to be this created_at. All right. So,
body. We may change the body down the line, but it's fine for now. We have updated_at, this updated_at, and created_at, which is going to be this created_at. All right. So, from the front end, this should look more or less exactly the same, but we have gotten rid of the User. Now, the user_id column is important, but by itself, it's sort of useless because I need to do two API calls in order to retrieve all of the information I need for almost every single place I use it. I would need the user's ID first, and then I'd need to make an API request to grab the User object itself and get something like this in order to display, say, the User's name from the front end. Now, thankfully, we can nest the User object, so this here, inside of the Post resource, and API resources make this super simple as well. So, I'm going to go back into the IDE, and we'll create a property.
nest the User object, so this here, inside of the Post resource, and API resources make this super simple as well. So, I'm going to go back into the IDE, and we'll create a property called user, and that user is going to be a UserResource, which wraps, well, the user relationship. Now, eagle-eyed viewers will already see a problem with this, but don't worry. We're going to come back and fix that a little bit later. So, just hold on to your browsers. Let's refresh the URL, pretty print, and now we have this object here, which is the user. So, from the front end, I can say, well, I have a Post. I can jump into the user property, and in the user property, I can grab their name, and that is the author of the given post. The other nice thing is it follows the same rules and conventions as our top-level UserResource. So, you can see here, well, I cannot see this user's email
the given post. The other nice thing is it follows the same rules and conventions as our top-level User resource. So, you can see here, well, I cannot see this user's email address because I am not this particular User. So, all of that security that we've carefully thought about is baked right in to nested resources. Super cool. Let's do the same for our Comment resource. So, what columns does our Comment resource actually need to get us up and running? Well, again, we need the ID. We also had the body, so we'll add that in now, and we had the created_at and updated_at timestamps, so updated_at, and that is equal to the updated_at column, and created_at, which is equal to the created_at column. Now, we can focus on our foreign keys once more. So, a user_id and a post_id. Now, we have both resources that we can edit in order to make this a reality. So, I'll
at column. Now, we can focus on our foreign keys once more. So, a user_id and a post_id. Now, we have both resources that we can edit in order to make this a reality. So, I'll put them up here under the ID. First of all, we'll have the User. That's going to be, as before, a User resource, which we'll make. All we have to do is access the user property on this Comment resource, and then we also have the Post that the comment belongs to. Well, that's going to be a Post resource, which we'll make, and that is going to access the post property on the underlying Comment model. So, let's go back to the browser, refresh PrettyPrint once more, and take a look at our new Comment resource. Here we have the ID. We also now have a User object, which has the same format as the User resource up here. We have the Post object, which has the same format as the Post up here, and it also loads
We also now have a User object, which has the same format as the user resource up here. We have the Post object, which has the same format as the post up here, and it also loads in its User. So, from a single Comment, we'd be able to access the Post with all of the information about the post and the User who created the post with the information we'd require about them as well. So, we've almost built this tree with branches that we can send to the front end. It won't contain any information that we don't know about, so we can avoid potential security leaks. And if we need to add a new column, say, to our user resource, well, anywhere where that user resource is updated, whether it's a User itself or a User inside a Post or a User inside a Post inside a Comment, it will be updated across the board and we'll be able to use it immediately from the front end. So, with our resources
Refactoring controller to resources13:43
a User inside a Post or a User inside a Post inside a Comment, it will be updated across the board and we'll be able to use it immediately from the front end. So, with our resources complete, let's go ahead and remove this test route before we forget about it and expose a very large security hole in our application. Then let's go back to the PostController index, and rather than return Post::all(), I'm going to return PostResource, collect, and then I'm going to wrap Post::all() in that collection. So, here's another static method you can use. If you're ever loading more than one model, you'll want to reach for collect rather than make. Hopefully, if I go back to the post endpoint, because we got rid of the data wrapping, everything still works exactly as it did before, but now we've refactored to API resources, and that is going to put us in great stead going forwards. Okay, I
wrapping, everything still works exactly as it did before, but now we've refactored to API resources, and that is going to put us in great stead going forwards. Okay, I think that's enough for this episode. We've covered quite a lot of ground in that refactor. In the next episode, we'll take a look at fixing performance problems. So, first of all, paginating this giant index and creating a Paginator we can reuse across the application, but we'll also talk about a performance issue that we've introduced in this episode. So, I'll see you there when we can fix all the problems we've just created.
