Module Options

In this chapter, you'll learn about passing options to your module from the MedusaGo application's configurations, and how to access and validate them in the module.

What are Module Options?#

A module can have options that customise its behaviour or make it adaptable. For example, a module that integrates a third-party service may have an option for the API key of the service, and the Blog Module of the previous chapters may have an option for the longest title a post can have.

Options are passed to a module from the configuration of the MedusaGo application, in the Options field of the module's entry. A module receives them in its service and its loaders as a map[string]any.


How to Pass Options to a Module?#

To pass options to a module, set the Options field of its entry in the Modules of the configuration in medusa-config.go:

medusa-config.go
// Config is the configuration of the application.
func Config() config.InputConfig {
	return config.InputConfig{
		ProjectConfig: config.ProjectConfig{
			DatabaseURL: os.Getenv("DATABASE_URL"),
		},
		Modules: []config.ModuleConfig{
			{
				// the path of the module directory: db:generate writes
				// the migrations there
				Resolve: "./src/modules/blog",
				Module:  &blog.Module.ModuleExports,
				Options: map[string]any{
					"defaultTags":    []string{"news", "release"},
					"maxTitleLength": 80,
				},
			},
		},
	}
}

The Options field is a map[string]any. The Blog Module has two options: defaultTags, a list of strings, and maxTitleLength, a number.

Note: When the configuration is written in medusa-config.json or medusa-config.yaml, the options of a module come from decoded JSON or YAML: numbers are float64, lists are []any, and objects are map[string]any. In Go code you can pass any value, such as the int and []string above. Validate the options so that the module accepts both.

Access Module Options in Main Service#

The Service function of the module's definition receives a ServiceOptions value. Its Options field holds the module's options.

In the Blog Module, the function parses the options and stores them in the service:

src/modules/blog/index.go
// 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
	},
})

Then, the service uses them in its methods. For example, CreateDraft rejects a title that's longer than the maxTitleLength option:

src/modules/blog/service.go
// CreateDraft creates a draft post. The longest accepted title comes from the
// module options.
func (s *BlogModuleService) CreateDraft(ctx context.Context, title, slug string) (*dal.Record, error) {
	if len(title) > s.options.MaxTitleLength {
		return nil, fmt.Errorf("the title is longer than %d characters", s.options.MaxTitleLength)
	}
	created, err := s.Internal("Post").Create(ctx, []*dal.Record{rec("title", title, "slug", slug)})
	if err != nil {
		return nil, err
	}
	return created[0], nil
}

Access Module Options in Loader#

A loader receives the options in the Options field of its LoaderOptions argument. The SeedTags loader uses them to read the defaultTags option:

src/modules/blog/loaders.go
// 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
}

Validate Module Options#

A module should check its options when the application loads it, so that a missing or invalid option fails the application at startup, with a clear error, instead of failing later at runtime.

Write the type of the options and the function that validates them:

src/modules/blog/options.go
import (
	"fmt"
)

// Options are the options of the blog module, the `options` of its entry in
// medusa-config.go.
type Options struct {
	// DefaultTags are created by the module's loader.
	DefaultTags []string
	// MaxTitleLength is the longest title CreateDraft accepts.
	MaxTitleLength int
}

// ParseOptions validates the options map the module receives.
func ParseOptions(raw map[string]any) (Options, error) {
	opts := Options{MaxTitleLength: 120}

	switch v := raw["defaultTags"].(type) {
	case nil:
	case []string:
		opts.DefaultTags = v
	case []any:
		for _, tag := range v {
			s, ok := tag.(string)
			if !ok {
				return opts, fmt.Errorf("defaultTags must be a list of strings")
			}
			opts.DefaultTags = append(opts.DefaultTags, s)
		}
	default:
		return opts, fmt.Errorf("defaultTags must be a list of strings")
	}

	switch v := raw["maxTitleLength"].(type) {
	case nil:
	case int:
		opts.MaxTitleLength = v
	case float64:
		opts.MaxTitleLength = int(v)
	default:
		return opts, fmt.Errorf("maxTitleLength must be a number")
	}
	if opts.MaxTitleLength <= 0 {
		return opts, fmt.Errorf("maxTitleLength must be greater than 0")
	}
	return opts, nil
}

ParseOptions returns the options with their defaults, or an error. It's used by the Service function and by the loader, so both validate the same way.

If the Service function returns an error, the application doesn't start. For an invalid maxTitleLength, the error is prefixed by the key of the module:

Code
blog: maxTitleLength must be a number
Note: Modules that have providers, such as the File Module, take the options of each provider in the Providers field of the module's configuration, next to the module's own Options. The two sets of options are separate.
Found a problem with this page?Report Issue
Edited Nov 14·Edit this page