Module Loaders
In this chapter, you'll learn about loaders and how to use them in a module.
What is a Loader?#
When you start the MedusaGo application, the modules it uses are loaded: each one builds its service and registers it in the container. Modules also run their loaders at certain points.
A loader is a function that a module defines to run code when the module is loaded. Use loaders to perform one-time setup, such as seeding the module's tables with default data, or checking that the module's configuration is valid.
MedusaGo's own modules use loaders for the same purposes. For example, the Currency Module's loader inserts the list of currencies, and the Region Module's loader inserts the list of countries.
How to Create a Loader?#
1. Implement Loader Function#
A loader is a function of type Loader, which receives a context and a LoaderOptions value:
| Field | Description |
| --- | --- |
| DB | The database handle of the module. |
| Options | The options of the module, as a map[string]any. |
| Providers | The providers registered for the module, for modules that have providers. |
| Logger | A *slog.Logger. |
A loader doesn't receive the container. To use the module's data models, build a service over them with NewMedusaService and use the internal service of the data model.
For example, create the file src/modules/blog/loaders.go with the following content:
// SeedTags creates the default tags of the module options. It is safe to
// run many times: tags that exist are left as they are.
func SeedTags(ctx context.Context, opts typesmodulessdk.LoaderOptions) error {
options, err := ParseOptions(opts.Options)
if err != nil {
return err
}
if len(options.DefaultTags) == 0 {
return nil
}
// A loader gets the database handle, not the module service: build the
// service of the models to use their internal services.
tags := utilsmodulessdk.NewMedusaService(opts.DB, Models, nil).Internal("Tag")
wanted := make([]any, len(options.DefaultTags))
for i, value := range options.DefaultTags {
wanted[i] = value
}
existing, err := tags.List(ctx, rec("value", rec("$in", wanted)), utilsmodulessdk.FindConfig{})
if err != nil {
return err
}
have := map[string]bool{}
for _, tag := range existing {
value, _ := tag.Get("value")
have[value.(string)] = true
}
var missing []*dal.Record
for _, value := range options.DefaultTags {
if !have[value] {
missing = append(missing, rec("value", value))
}
}
if _, err := tags.Create(ctx, missing); err != nil {
return err
}
opts.Logger.Info(fmt.Sprintf("blog: created %d default tags", len(missing)))
return nil
}
The SeedTags loader creates the tags listed in the defaultTags option of the module. It first lists the tags that exist, and creates only the missing ones. That makes it safe to run many times.
2. Export Loader in Module Definition#
To use the loader, list it in the Loaders field of the module's definition:
// Module is the definition of the blog module.
var Module = utilsmodulessdk.Module(Name, utilsmodulessdk.ModuleDefinition{
Models: Models,
Migrations: migrations.Migrations,
Loaders: []typesmodulessdk.Loader{SeedTags},
Service: func(opts typesmodulessdk.ServiceOptions, base *utilsmodulessdk.MedusaService) (any, error) {
options, err := ParseOptions(opts.Options)
if err != nil {
return nil, err
}
return &BlogModuleService{MedusaService: base, options: options}, nil
},
})
The loaders of a module run in the order of the list.
Test the Loader#
Run the migrations, which run the loaders of every module:
The loader logs how many tags it created. Run the command again, and the loader creates none, since the tags exist.
When are Loaders Executed?#
Loaders Executed with Migrations#
MedusaGo runs the loaders of the modules after it runs the migrations and syncs the links, in these commands:
go run . db:migratego run . db:setup, which runs the same steps asdb:migrate, after it creates the database.go run . db:migrate:scripts
The loaders run in the order the modules appear in the application's configuration.
Loaders Executed on Application Startup#
A loader doesn't run when the application starts unless the module asks for it. The built-in modules whose loaders insert required data, such as the Payment Module, ask for it. Set LoadersOnStart to true on the module's exports to do the same:
func init() {
// Run the loaders every time the application starts, not only with
// `db:migrate`.
Module.LoadersOnStart = true
}
The loaders of the module then run each time the application starts, after the modules' services are created.
LoadersOnStart, and for all modules when you run the migration commands above.Avoid Heavy Operations in Loaders#
A loader that runs on startup delays the start of the application, and a failing loader stops it. Keep such loaders fast and make them tolerate a temporary failure.
The loaders of MedusaGo's own modules return nil after they log a warning when they can't insert their data, so that the application can still start. Return an error from a loader only when the module can't work without what the loader does.
Alternative Solutions#
If you need to run heavy work, consider these alternatives:
- A scheduled job, to run the work periodically.
- A custom CLI script, to run the work when you decide to.
- A workflow, to run the work in the background.
Create Resources in the Service#
Upstream docs use loaders to create a client for an external service, such as a database connection, and register it in the container for the module's service. In MedusaGo, create such a resource in the Service function of the module's definition instead, and keep it in your service struct.
The function receives the ServiceOptions of the module, with the DB, Options, Logger, Providers, and Container fields. The following Service function parses the module's options and stores them in the service:
// Module is the definition of the blog module.
var Module = utilsmodulessdk.Module(Name, utilsmodulessdk.ModuleDefinition{
Models: Models,
Migrations: migrations.Migrations,
Loaders: []typesmodulessdk.Loader{SeedTags},
Service: func(opts typesmodulessdk.ServiceOptions, base *utilsmodulessdk.MedusaService) (any, error) {
options, err := ParseOptions(opts.Options)
if err != nil {
return nil, err
}
return &BlogModuleService{MedusaService: base, options: options}, nil
},
})
The function runs once when the application loads the module. Return an error from it to stop the application, for example when an option is invalid.