Introduction & Project Setup
Hello everyone. My name is Braden Gerard, and today we're going to look at how you can create custom controllers inside of Strapi. So I have a brand new Strapi project set up here. I just created it with npx create strappy app. I just ran it and I have the brand new account set up here with no content in here yet.
So what I'm going to do first is I'm just going to create a content type. So we'll go over here and we'll create a new collection type. We're going to call that 'to-do' and hit continue. Now I'm going to give that to-do a description and I'll hit add another field, and then I'm also going to give it a boolean which is whether or not it has been completed. So 'complete', and that's a very simple to-do object that we'll create here as a collection.
And Strapi's just going to restart now that I've created that. So we can go back over to our code here. Actually, first let's add one to-do in here just so we have something to test with. So we'll create a new to-do, we'll say 'walk the dog', and we'll set it to false, it has not been completed yet. We'll save that and we'll go over here and see that we now have a to-do with the ID of one and it's 'walk the dog'. So that's good. Oh, we need to publish it. So we'll publish that. There we go. And now we have 'walk the dog' as a to-do item.
So let's go back over to our code now and we can test this out to see if our to-do is working. Now, so we'll go over here and we'll say localhost:1337/api/todos. We'll send that request and we'll see that we get a 200 response. So let me just close down this terminal for you. And we can see that we get our to-do here back with the ID of one and the attributes of 'walk the dog' as a description and 'complete' as false, and then some generated date timestamps here. So that's all working. That's great. That's getting all of our to-dos.
Testing Default API Endpoints
And we can also test out if we want to just find one to-do, we can go to /api/todos/1 and now we get that same to-do back, but instead of getting an array like we got in the other response here, we just get a single object. Okay, so we know that we can get our to-dos, but what if we wanted to get our to-dos in, you know, a unique way? Maybe we want to add some extra information in here, or maybe we want to do some logging whenever someone requests their to-dos.
So first, let's look at our controllers. So to customize our controllers, what we're going to do is we're going to go into source and we're going to go into the api folder, and then we'll go to the to-do folder, and inside the to-do folder we'll go to our controllers.
Extending the Core Controller
Now in controllers we have a to-do.js and this is just the default Strapi generated code. Now we can extend this and we can customize the controllers for the to-do collection by adding our own custom actions is what they're called. So to extend this, all we have to do is go here, and we're going to say comma, and then we're going to say... the Strapi object should be equal to... and then inside of here we can add our own custom actions.
So let's put... Sorry, we need to make this like this and like this. And inside of here we can add our own custom actions. So there's three different ways we can add our own custom actions. We can create an entirely custom action if we want. So let's do an example of that first.
Creating a New Custom Action and Route
So we'll make an async function in here and we give it the name of our action. So we'll call this, I don't know, 'customAction', okay? And then we're going to receive the context and that context object will be used to get the body of the request or to get anything out of the request, like query parameters or anything like that. And then we will do a try-catch here and we'll check what's in the body. So we'll say ctx.body... or sorry, we'll set the body. So we'll say ctx.body is equal to 'okay'. And then we will say ctx.body is equal to error. So if for some reason our custom action fails, it'll send back the error in the body. If not, our custom action will send back 'okay', the text 'okay' in the body.
Okay, so we've called this customAction. Now if we want to make an endpoint for that custom action, then we have to go over to routes and inside of our to-do.js we need to add that custom action to our routes. So we can do that by creating our own file in this routes folder. So in this routes folder, we can make a new file and we'll just call it custom.js.
And then inside of that custom.js file, we'll do module.exports = and an object. And inside of that object, we have a property called routes and that property takes an array of objects. And each object can be a route. So we'll set the method on the route to 'get', and then we set the path on the route to the path that we want to be able to access that route from. So we'll just say /custom for our example here. And then we can specify the handler that we want to be called whenever that route gets hit, which in this case is our custom controller that we just created, or custom action in our controller. So we'll say customAction. customAction, and you need to say what controller that's on. So we'll say to-do.customAction and save that.
Now there's one other thing that we can do is we can give it a config object where we can configure different options with this route. And what we want to do is say auth and put that to false just so that we can hit this without being authenticated. So we'll save that. Our server is going to restart and we can go over here and we can test that out. So we can test that out by saying go to the localhost:1337 route, /api/custom, and if I send that, we'll see that we get a 200 response back and the body is 'okay'. So we can see that we're actually hitting our custom action. So I change this to 'okay hello world' and we save that, and then I go back over here and make that request again, we get 'okay hello world'.
So that's how you can add a completely custom action into a Strapi controller.
Wrapping a Core Action (find)
Now we can also wrap an existing action that Strapi has built in. So by default, we saw earlier we can do a find on our to-dos. So we can overwrite that find action. So we just say async find and then inside of that we can write what we want to write to override that action.
So that we can get the query from the find. So let's go ctx.query = and we'll say take the query that came in, and let's also add locale as en. So we'll add that into the query. And then maybe we continue on doing what the default action would do. So we'll say const { data, meta } = await super.find(). So we call that super.find to do the default functionality and get the data and meta out of that. And then we'll say meta.date = and then let's just say Date.now(). So we're modifying here what we normally get back in the response. And then from that we can say return, and return back data and meta.
So what we've done here is we've added in the en locale to our query and then we did our find and then we set the date to the current date and then return back that data as well as the meta information. So that modifies the existing, the current find functionality, right? So if we go back over here, save that, and go back over to our request here, we can do a find on all. And if we saw from before here, let me just close down the terminal here. We can see that our last find all got this meta information and these attributes with the ID of the object of the one to-do that we have in our array. If I do the request now, we'll see that we're getting back some additional information. We're getting back the current date here inside of our meta object. And we got data with the locale of english. Okay, so this is how you can overwrite an existing Strapi action.
Replacing a Core Action (findOne)
And then the last thing we can do is we can actually replace a core action. So here we still sort of, we still called into super to run the previous find action. If we wanted to completely do our own action, we could say async... sorry, if we wanted to completely override the existing action but with our own functionality, we could do async findOne, get the context.
So we'll overwrite the findOne action, and we'll say const { id } = ctx.params that came in from the request, and then we'll get the query from the context. And then we will say const entity = await strapi.service('api::to-do.to-do').findOne(). I'm going to get the id to find by the id and the query. So that's using the Strapi service to go in and find one. So we could do this however we like. It's not using just the super.find. And then we're going to get the entity out from that.
And then from that entity, we have to say const sanitizedEntity, just so that we can make sure it's sending it back in the correct format. So sanitize output, entity, and the context. And then we're going to return this.transformResponse(sanitizedEntity).
So this is doing what the super.find would do, but completely on our own. So we're using the Strapi service and we're saying findOne with this ID and this query. And those are IDs and queries that we got out of the context. We take that entity, we run the sanitizeOutput on it with the context, and then we send back the sanitized entity.
Debugging & Conclusion
So if we save that and we go over to our findOne and we see here we're getting back this information, if I hit a send again, we're gonna get an internal server. I made a mistake here somewhere. Let's take a look quickly.
Where is my server error? And we have an error and oh, this.sanitize... oh, it's a sanitizeOutput, not sanitized. So I made a typo here. sanitizeOutput. And then save that and make sure this is running again. Good. Then we'll go back over to find one, send that request, and you'll see you'll get back the same response here that you would from the built-in Strapi findOne action, but we're completely doing it on our own here. So this just allows you to add in any type of customization that you want to override the default findOne action. So that's how you can customize controllers inside of Strapi.