API Routes | Medusa Development Course
By Viktor Holik
By Viktor Holik
Welcome to our Medusa Development Course! In this series, you'll learn how to get your development environment ready and take your first steps in building a Medusa-powered e-commerce application. We'll walk you through everything step by step, helping you master Medusa’s modular architecture and build the key features you need.
This course was made by Viktor Holik, our Software Engineer and Medusa Expert.
Explore the essential aspect of building and customizing API routes within Medusa. By building endpoints, setting up secure authentication, and utilizing validation, you can tailor your Medusa store's functionality.
You can find the transcription of the guide below.
In this chapter, we'll be discovering what are API routes in Medusa.
Let's decompose our store custom endpoint and actually create a custom store suppliers endpoint. We can do that by creating a new folder in the store folder. Let's call it Suppliers, and in the Suppliers folder, create a new file called route.ts. To create a new endpoint under the store/suppliers namespace, we should export a function from that file with the HTTP method in the name. In our case, it would be get.
Let's resolve the SupplierService from the dependency container. Also, let's not forget to use the generic type to get typings of the service. Let's use the SupplierService to list all suppliers and return to the client.
I also want to mention that Medusa not only allows the use of the GET HTTP method. You can use multiple methods like POST, PUT, PATCH, etc. So, let's not only have the endpoint for listing all suppliers, but also an endpoint for retrieving a specific supplier by his ID.
For this use case, I'll use the pass params in Medusa. I'll create a new folder with [id] and create a new file route.ts. I will copy the listing suppliers endpoint and only modify the import. Also, I will assign the ID from the pass params from the request. And, what we see, we do not have a retrieve method in the SupplierService, so let's add it. Now we can use this method to retrieve a specific supplier.
Don't forget to change the variable name from suppliers to supplier. Let's also add an endpoint for updating a specific supplier by his ID. I'll copy the GET function, change the name to POST, and add an update method that would update the supplier by ID. I will use the parameters id and data.
I'll remove the use of the SupplierRepository for achieving the specific record and instead I'll reuse the retrieve method from the SupplierRepository. Now, let's iterate every key in the data object and replace all the values with new, then later use the save method in the SupplierRepository to save the changes in the database.
If we hit the store/suppliers endpoint, we would return two keys with the same name, one of them is unnecessary, so let's fix it. Instead of returning an object, let's return a tuple where the first element is the list of records and the second element is the total count. Now, let's refactor our code in the endpoints and add count to the object response.
Now I get typescript error in the store custom endpoint because we refactored the methods. I'll just fix it by removing the response in the endpoints. Now, if we hit the store/suppliers endpoint, we would see beautifully displayed data - suppliers and count.
Next, let's refactor updating supplier endpoint, which uses suppliers (id) and is a POST request. I'll use the update method from the SupplierService and in method as a body I will provide the request body.
Let’s take a look if the update works—if I provide a name for the specific supplier, it changes, and we can see it in the response of the request.
Let's create a new method in a SupplierService, which would be responsible for saving new records in the database. I’ll use the create method from the SupplierRepository and also save method. Now, I'll go to the store/suppliers/route.ts file and add a new POST method, which would handle the creation of the suppliers in the database and we'll be utilizing the create method.
But now we have a problem: we can send any form of request body, and it would pass to the database level and potentially lead to some errors and vulnerabilities, so we should definitely fix this. I'll create a new file called validators.ts in the Suppliers folder and create a new class called StorePostSupplierRequest. I'll use a convention to add a scope of the endpoints—in our case, it would be store, the method, and the entity name. I’ll use a string decorator to test if the name field is a string.
Now, I can go to the store/suppliers POST method and import the validator function from the medusajs/medusa package. I also want to draw attention that validator is a promise, so we should use await before using the function. As the first argument, I'll pass the validation class, and as the second, I'll pass the request body. The validator returns the validated data, so we can actually paste it in the create method.
Now let's do the same procedure with the update endpoints. I will copy the validation logic in the update endpoints and also I should create a new validator specifically for the update endpoints. It will be very similar, except I will use the StorePostSupplierRequest convention and pass the same values as in the create validator. Now I will import the validator function from the package and also the validation class. Let's not forget about passing the validated data in the update method.
If we try to send in proper data form, it would get an error and the 400 status. So we should change it to the proper and use name field with the proper string. And now we would see it's 200 request status.
Let’s finish our CRUD store/suppliers endpoints and add new deletion endpoints. I’ll create a new method in SupplierService and notice that we are using soft deletion in our entity model. Here we are extending from SoftDeletableEntity so we should use softDelete method instead of delete. And I would use the SupplierServiceDelete method, and also let's change the response to success true instead of returning the deleted supplier.
Now, let’s test it in Postman. I would delete specific supplier and now we can see the success true. And now if I would try to retrieve the specific supplier, I would get just null. Also we cannot see the specific record in the list endpoint. But hey, I don't want to return 200 request status with the null in the supplier property, so I should definitely throw some error.
In order to do that, I should import MedusaError from the medusa-core-utils package. Actually it's also fixed relative pass in the supplier models import. And now I should specify the MedusaError type. In our case it would be not found. And also as a second argument, I should specify a message that supplier is not found.
Also I want to show you what it would look like if we would not use them MedusaError and just use default error in the JavaScript. And I would provide the supplier not found message. Let's start the backend. Let's add to the postman and test our endpoint and we get an unknown error, and that's because Medusa treats non-medusa errors as the potential sensitive information, so it marks them as an unknown error.
I will remove the error and go back to the MedusaError and let's see how does it work in the postman. Now when we hit the endpoint with non-existing supplier ID, we would get the 404 status, the type of not found and the correct message.
As Medusa is a headless commerce engine, it comes with a already built endpoint groups. The first endpoint group type is the store and the second one is admin. Store endpoint group is typically used on a storefront. So for example, reading of published products, reading of me as a customer in my orders, reading my card, updating my card, and reading for example region. So you can imagine it as a customer-related logic.
Another group we have admin endpoint group that is used for the managing orders, and not only my orders, but all orders in the store, reading products, editing products, and deleting them for example, managing regions, store configuration. As we don't want to allow customers to manage our suppliers in the store, we should definitely move the suppliers folder into the admin, in order to allow only admins manage the suppliers.
Cool, let's now check how does it work in the Postman. If we hit the store/suppliers endpoint, we get a 404 status. So let's check the admin suppliers, and as we can see now, we get 401 status, that means that we are not authorized to do that request. To log in as an admin, we should hit the admin/auth endpoint and we should provide the credentials that we were using for initializing Medusa in the first chapter. If you followed me, the credentials would be - email: admin@medusatest.com and password: supersecret.
If we hit this endpoint, it would create a new session and attach it to the cookie, and now we can actually access the admin group. As you can see, we can create new supplier and get a list of suppliers, so all of that supplier-related logic works. Also, guys, I want to show you that not only admin API group is required for authentication, but some of the roads for the storefront. For example, the store/customers/me.
Even if you would try to access the endpoint with the prefix of store/customers/me, you would still get unauthorized error. In case if you want to disable the required authentication in the admin folder, or for example, the store/customers/me endpoint, you can use the authenticate flag, which would opt out the required authentication. And now, if I go to Postman, even when I clear all of my cookies, I would still be able to, for example, add new suppliers.
The next thing I want to show you guys is the middlewares in Medusa. For those who don't know what the middleware is, it's an action that runs before the route handler, and among other users, could modify the response of an API route.
You can imagine a situation when you want to return 41 status, if the user is not authenticated, and you can do it using the middlewares. In order to create our first middleware, we should create a new file called middlewares.es in the API folder. And I've just actually copied an example of middleware in Medusa from the docs. And as you can see here, I've just created a middleware that opts out the course configuration under the matcher. I will change it to the admin suppliers.
Let's remove that cors opt out and create our first new middleware. We can do that by creating new function that takes three arguments. First of which is the request, the second one is the response, and the third one is next function. If the next function is executed, it will proceed to the route handler.
Let's pass that myCustomMiddleware in the middlewares array. I will hit this endpoint with a postman, and now when I go to the console, I see the log from myCustomMiddleware.
Medusa comes with the default middlewares, like for example, require a customer authentication, which by the name you can understand what it does. The next customer authentication middleware is authenticate customer. What it does, it registers the customer in the request, but it does it optionally. So for example, if you don't require a customer to be authenticated, you can use that middleware. The next authentication middleware in Medusa is authenticate, which is quite the same with the required customer authentication. Besides, it works only on admin users.
Two previous middlewares only works for the customer. If you want to, for example, require admin user authentication, you can use authenticate middleware, and it requires the authentication for the admin users.
I also want to show you guys how the registration of the customers in the request works. So I will use the required customer authentication middleware under the store custom endpoints. Let's also remove some unnecessary imports. And also, let's add the console log of request.user in the store custom endpoint route handler.
Before testing the required customer authentication, we should authenticate as a customer, so I will create a new customer with that data. And now, as we have authenticated customer session cookie, we can hit the store custom endpoint and see the console. And we have an object in which we have customer ID as the authenticated customer from the session cookie.
You may have this situation where you registered a resource in the dependency container, but you want to, for example, access it in a service. On every request, Medusa creates a new scope in the container. Medusa creates a new scope of the dependency container on every request, so you can register some resources in the dependency container and only access it in the scope of the request.
Let me show what I mean. For example, I want to know what exactly the logged in customer looks like when he access some methods in the supplier service. So I will create a new property customer and assign this property from the dependency container. I will add a console log on the retrieve method because we use that method in the endpoints. And also what I should do is wrap the signing of the customer from the dependency container with the try catch. It would avoid the error on the Medusa start. It's also changed the property name from customer to logged in customer to be more precise.
Medusa codebase uses a convention in the properties and adds the underscore in the end of it to mark it as a private. So I should do this too.
Let's create a middleware that would access the logged in customer and then register him in a scope of the dependency container of the request. I created a new folder called middlewares and also in the folder out create a new file called register-logged-in-customer.ts. And now this should be a function that returns a function which takes three arguments: request, response, and the next function.
Now what we should do is access the customer ID from the request user property and then using the customer service, retrieve that logged in customer and then register him in the dependency container scope.
Let's assign customer service to the variable of customer service. Also, let's not forget about providing a type definition from the Medusa package of the service. And now we can retrieve that user using the customer ID.
Now using the request scope register, we can provide the registered resource name, in our case, it would be logged in customer. And as a second parameter would provide a result function that would return this customer.
Also, let's not forget about executing the next function in the end of the middleware to proceed to the route handler. And also, let's make a statement if the customer ID could not be accessed from the request user property, it would throw the unauthorized MedusaError.
I will add new endpoint called store suppliers and provide the same logic from the admin suppliers get request to retrieve suppliers. Let's provide correct imports. Also, let's fix the relative imports to not have any errors. And now I will add the console of this logged in customer into the list and count method of the service. As we provided the resource name of logged in customer and not just customer, we should change the name in the injected dependencies.
Let's go to the middleware.ts file and add a new middleware to the store suppliers endpoint. We can do that by providing the measure of store suppliers and adding the array of middlewares one by one. The first one would be authenticate customer, and the second one would be register-logged-in-customer. Let's import it from the middlewares folder and also let's change the past relative.
Now when I hit the store suppliers endpoint and go to the console, I could see the list and count message with the customer object which depends on the authenticated customer in the request.
Also guys, I want to warn you that if you are extending the Medusa default services like for example product service, due to the fact that Medusa default services are cast in dependency container and they are using the lifetime of single tone?
If you are registering some resources in the scoped dependency container and you want to access them in the default Medusa services that you are extending, you ahould change the lifetime to transient or scoped, in that case you opt out the caching of the dependency container.
As I have shown you before, Medusa counts with default API endpoints for the storefront and the admin panel and you might have a case where you want to change some of the validators of API endpoints. For example admin products.
If you would visit the Medusa query repository and check the admin products endpoint for creating products, we would see that there is admin post products request class that is responsible for validating the body request. I want to show you how you can extend it.
I will create new folder in the admin directory called products and place new file called validator.ts in the directory. I follow this convention to be able in a feature identified what validators are related to what endpoints, in our case it would be admin products. And what I should do is create new class called admin create product and I would extend it from the Medusa admin create product. Let's import the Medusa admin create product from the API folder.
And actually it's admin post products request instead of admin create product. So I will change it to Medusa admin post product request. Now I would be able to extend validation class with the additional fields. It could be actually anything. So I will use additional fields as the key name and also I should import the IsString decorator from the class-validator data package.
Now what I should do next is create new index.ts file under the API directory, import the register of reading validators function from the Medusa package, and then executed was the class that we actually created a second before. In our case it would be admin post products request and also let's not forget about importing this class from the API admin products validator file.
Now I should login as an admin using the admin endpoint and also I should hit the admin products endpoint in order to create a product. I will provide some test data like name test but the response status is 400 and it's telling me to add additional field which must be a string.
This tutorial walked you through creating a custom store/suppliers endpoint, managing CRUD operations, and implementing advanced configurations such as validation and authentication. By using dependency injection, middleware, and validators, you learned to refine your endpoints for secure, efficient handling of database interactions.
Stay tuned as we continue this Medusa journey, where we’ll develop Medusa services to include business logic and core e-commerce functionality.
We hope you found Viktor's tutorial insightful and helpful.
Welcome to our Medusa Development Course! In this chapter, we'll talk about entities in Medusa to model your data and manage relationships between products, orders, and customers...
Welcome to our Medusa Development Course! In this episode, we’ll be discussing an essential part of Medusa’s architecture: dependency injection and the dependency container...