www/apps/book/app/learn/fundamentals/modules/loaders/page.mdx
import { Prerequisites } from "docs-ui"
export const metadata = {
title: ${pageNumber} Loaders,
}
In this chapter, you’ll learn about loaders and how to use them.
When building a commerce application, you'll often need to execute an action the first time the application starts. For example, if your application needs to connect to databases other than Medusa's PostgreSQL database, you might need to establish a connection on application startup.
In Medusa, you can execute an action when the application starts using a loader. A loader is a function exported by a module, which is a package of business logic for a single domain. When the Medusa application starts, it executes all loaders exported by configured modules.
Loaders are useful to register custom resources, such as database connections, in the module's container, which is similar to the Medusa container but includes only resources available to the module. Modules are isolated, so they can't access resources outside of them, such as a service in another module.
<Note title="Why are modules isolated?">Medusa isolates modules to ensure that they're re-usable across applications, aren't tightly coupled to other resources, and don't have implications when integrated into the Medusa application. Learn more about why modules are isolated in this chapter, and check out this reference for the list of resources in the module's container.
</Note>You create a loader function in a TypeScript or JavaScript file under a module's loaders directory.
For example, consider you have a hello module, you can create a loader at src/modules/hello/loaders/hello-world.ts with the following content:
Learn how to create a module in this chapter.
</Note>import {
LoaderOptions,
} from "@medusajs/framework/types"
export default async function helloWorldLoader({
container,
}: LoaderOptions) {
const logger = container.resolve("logger")
logger.info("[HELLO MODULE] Just started the Medusa application!")
}
The loader file exports an async function, which is the function executed when the application loads.
The function receives an object parameter that has a container property, which is the module's container that you can use to resolve resources from. In this example, you resolve the Logger utility to log a message in the terminal.
Find the list of resources in the module's container in this reference.
</Note>After implementing the loader, you must export it in the module's definition in the index.ts file at the root of the module's directory. Otherwise, the Medusa application will not run it.
So, to export the loader you implemented above in the hello module, add the following to src/modules/hello/index.ts:
// other imports...
import helloWorldLoader from "./loaders/hello-world"
export default Module("hello", {
// ...
loaders: [helloWorldLoader],
})
The second parameter of the Module function accepts a loaders property whose value is an array of loader functions. The Medusa application will execute these functions when it starts.
Assuming your module is added to Medusa's configuration, you can test the loader by starting the Medusa application:
npm run dev
Then, you'll find the following message logged in the terminal:
info: [HELLO MODULE] Just started the Medusa application!
This indicates that the loader in the hello module ran and logged this message.
When you start the Medusa application, it executes the loaders of all modules in their registration order.
A loader is executed before the module's main service is instantiated. So, you can use loaders to register in the module's container resources that you want to use in the module's service. For example, you can register a database connection.
Loaders are also useful to only load a module if a certain condition is met. For example, if you try to connect to a database in a loader but the connection fails, you can throw an error in the loader to prevent the module from being loaded. This is useful if your module depends on an external service to work.
Loaders are also executed when you run migrations. This can be useful if you need to run some task before the migrations, or you want to migrate some data to an integrated third-party system as part of the migration process.
Since loaders are executed when the Medusa application starts, heavy operations will increase the startup time of the application.
So, avoid operations that take a long time to complete, such as fetching a large amount of data from an external API or database, in loaders.
Instead of performing heavy operations in loaders, consider one of the following solutions:
As mentioned in this chapter's introduction, loaders are most useful when you need to register a custom resource in the module's container to re-use it in other customizations in the module.
Consider your have a MongoDB module that allows you to perform operations on a MongoDB database.
<Prerequisites items={[ { text: "MongoDB database that you can connect to from a local machine.", link: "https://www.mongodb.com" }, { text: "Install the MongoDB SDK in your Medusa application.", link: "https://www.mongodb.com/docs/drivers/node/current/quick-start/download-and-install/#install-the-node.js-driver" } ]} />
To connect to the database, you create the following loader in your module:
<Note>As of Medusa v2.11.0, Awilix dependencies are included in the @medusajs/framework package. If you're using an older version of Medusa, change the import statement to awilix.
export const loaderHighlights = [
["5", "ModuleOptions", "Define a type for expected options."],
["13", "ModuleOptions", "Pass the option type as a type argument to LoaderOptions."],
["23", "clientDb", "Create a client instance that connects to the specified database."],
["29", "register", "Register custom resource in the container."],
["30", "mongoClient", "The resource's key in the container."],
["31", "asValue(clientDb)", "The resource to register."]
]
import { LoaderOptions } from "@medusajs/framework/types"
import { asValue } from "@medusajs/framework/awilix"
import { MongoClient } from "mongodb"
type ModuleOptions = {
connection_url?: string
db_name?: string
}
export default async function mongoConnectionLoader({
container,
options,
}: LoaderOptions<ModuleOptions>) {
if (!options.connection_url) {
throw new Error(`[MONGO MDOULE]: connection_url option is required.`)
}
if (!options.db_name) {
throw new Error(`[MONGO MDOULE]: db_name option is required.`)
}
const logger = container.resolve("logger")
try {
const clientDb = (
await (new MongoClient(options.connection_url)).connect()
).db(options.db_name)
logger.info("Connected to MongoDB")
container.register(
"mongoClient",
asValue(clientDb)
)
} catch (e) {
logger.error(
`[MONGO MDOULE]: An error occurred while connecting to MongoDB: ${e}`
)
}
}
The loader function accepts in its object parameter an options property, which is the options passed to the module in Medusa's configurations. For example:
export const optionHighlights = [ ["6", "options", "The options to pass to the module."] ]
module.exports = defineConfig({
// ...
modules: [
{
resolve: "./src/modules/mongo",
options: {
connection_url: process.env.MONGO_CONNECTION_URL,
db_name: process.env.MONGO_DB_NAME,
},
},
],
})
Passing options is useful when your module needs informations like connection URLs or API keys, as it ensures your module can be re-usable across applications. For the MongoDB Module, you expect two options:
connection_url: the URL to connect to the MongoDB database.db_name: The name of the database to connect to.In the loader, you check first that these options are set before proceeding. Then, you create an instance of the MongoDB client and connect to the database specified in the options.
After creating the client, you register it in the module's container using the container's register method. The method accepts two parameters:
mongoClient. You'll use this name later to resolve the client.asValue function imported from the @medusajs/framework/awilix package, which is the package used to implement the container functionality in Medusa.After registering the custom MongoDB client in the module's container, you can now resolve and use it in the module's service.
For example:
export const serviceHighlights = [ ["10", "mongoClient", "Resolve the MongoDB client from the container."], ["11", "mongoClient_", "Set the MongoDB client as a class property."], ["14", "createMovie", "Add a method that uses the MongoDB client to create a document."], ["30", "deleteMovie", "Add a method that uses the MongoDB client to delete a document."] ]
import type { Db } from "mongodb"
type InjectedDependencies = {
mongoClient: Db
}
export default class MongoModuleService {
private mongoClient_: Db
constructor({ mongoClient }: InjectedDependencies) {
this.mongoClient_ = mongoClient
}
async createMovie({ title }: {
title: string
}) {
const moviesCol = this.mongoClient_.collection("movie")
const insertedMovie = await moviesCol.insertOne({
title,
})
const movie = await moviesCol.findOne({
_id: insertedMovie.insertedId,
})
return movie
}
async deleteMovie(id: string) {
const moviesCol = this.mongoClient_.collection("movie")
await moviesCol.deleteOne({
_id: {
equals: id,
},
})
}
}
The service MongoModuleService resolves the mongoClient resource you registered in the loader and sets it as a class property. You then use it in the createMovie and deleteMovie methods, which create and delete a document in a movie collection in the MongoDB database, respectively.
Make sure to export the loader in the module's definition in the index.ts file at the root directory of the module:
import { Module } from "@medusajs/framework/utils"
import MongoModuleService from "./service"
import mongoConnectionLoader from "./loaders/connection"
export const MONGO_MODULE = "mongo"
export default Module(MONGO_MODULE, {
service: MongoModuleService,
loaders: [mongoConnectionLoader],
})
You can test the connection out by starting the Medusa application. If it's successful, you'll see the following message logged in the terminal:
info: Connected to MongoDB
You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database.