Modules

In this chapter, you'll learn about modules and how to create them in your MedusaGo application.

What is a Module?#

A module is a reusable package of functionalities related to a single feature or domain. MedusaGo integrates the module into your application without implications on the existing setup, since the module is isolated from the rest of the application.

For example, MedusaGo's Commerce Modules are modules for commerce features, such as products, carts, or orders. You can also create custom modules to add features specific to your business, such as a blog or a loyalty programme.

A module has:

  • Data models that represent its tables in the database.
  • A service, a Go type that holds the module's business logic: it creates, reads, updates, and deletes the module's data, and exposes any custom method you add.
  • Optionally, migrations, loaders, and options.

MedusaGo registers the module's service in the container under the module's key, so you can use it in API routes, workflows, and scripts.

Note: MedusaGo modules are written with the same building blocks as Medusa's: DML data models, a service generated by MedusaService, and migrations. If you've written a Medusa module, only the language changes.

How to Create a Module?#

In this section, you'll build a Blog Module that stores blog posts, their comments, and their tags. You'll create it as a Go package at src/modules/blog in your application.

Note: The snippets in this chapter leave out the import block of the files that import other packages of the module. The import paths of your own packages start with the Go module path of your application. The import paths of MedusaGo's packages are the ones shown in the snippets that include them.

1. Create Data Model#

A data model represents a table in the database. You create it with the Data Model Language (DML), the dml package of MedusaGo.

Create the file src/modules/blog/models.go with the following content:

src/modules/blog/models.go
package blog

import "github.com/hitkey-io/medusago/packages/core/utils/src/dml"


var Post = dml.Define("post", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "post"}).PrimaryKey()},
	{Name: "title", Property: dml.Text().Searchable()},
	{Name: "slug", Property: dml.Text().Unique()},
	{Name: "content", Property: dml.Text().Nullable()},
	{Name: "status", Property: dml.Enum("draft", "published", "archived").Default("draft")},
	{Name: "views", Property: dml.Number().Default(0)},
	{Name: "published_at", Property: dml.DateTime().Nullable()},
	{Name: "author_id", Property: dml.Text().Nullable()},
	{Name: "metadata", Property: dml.JSON().Nullable()},
	{Name: "comments", Property: dml.HasMany("Comment", dml.RelationshipOptions{MappedBy: "post"})},
	{Name: "tags", Property: dml.ManyToMany("Tag", dml.RelationshipOptions{MappedBy: "posts"})},
}).
	WithIndexes([]dml.Index{{On: []string{"status", "published_at"}}}).
	WithCascades(dml.Cascades{Delete: []string{"comments"}})


var Comment = dml.Define("comment", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "cmt"}).PrimaryKey()},
	{Name: "author_name", Property: dml.Text()},
	{Name: "body", Property: dml.Text()},
	{Name: "post", Property: dml.BelongsTo("Post", dml.RelationshipOptions{MappedBy: "comments"})},
})


var Tag = dml.Define("tag", dml.Schema{
	{Name: "id", Property: dml.Id(dml.IdOptions{Prefix: "tag"}).PrimaryKey()},
	{Name: "value", Property: dml.Text().Unique()},
	{Name: "posts", Property: dml.ManyToMany("Post", dml.RelationshipOptions{MappedBy: "tags"})},
})


// Models lists the data models of the module.
var Models = []*dml.Entity{Post, Comment, Tag}

You define the Post data model with the dml.Define function. Its first argument is the name of the table. The second argument is the model's schema: the list of its properties, in order. Comment and Tag are related to Post, which you'll learn about in the Relationships chapter.

Models lists the data models of the module. You'll pass it to the module definition.

Learn more about data models in the Data Models chapter.

2. Create Service#

You create the module's main service in the file src/modules/blog/service.go. The service is a struct that embeds *MedusaService, the service factory. Embedding it gives your service methods to manage the records of every data model, without writing them yourself.

For example, create the file src/modules/blog/service.go with the following content:

src/modules/blog/service.go
// BlogModuleService is the service of the blog module. Embedding
// *MedusaService gives it the generated methods (listPosts, createPosts, ...)
// and the internal service of each model.
type BlogModuleService struct {
	*utilsmodulessdk.MedusaService
	options Options
}

The BlogModuleService struct embeds *utilsmodulessdk.MedusaService. The options field holds the module's options, which you set in the module's configuration in a later step.

You add your own methods to the struct, next to the ones the service factory gives it. For example, the CreateDraft method of the Blog Module creates a post whose title is no longer than the configured limit.

Learn more about the service factory in the Service Factory chapter.

3. Export Module Definition#

The final piece of a module is its definition. It tells MedusaGo which data models and service the module has. You create it in the file src/modules/blog/index.go:

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
	},
})

You call the Module function with:

  1. The module's name: "blog". It's the key under which MedusaGo registers the module's service in the container, and the name of the module in the configurations. It's also the prefix of the names of the module's events, such as blog.post.created.
  2. The module's definition, a ModuleDefinition with the following fields:

| Field | Description | | --- | --- | | Models | The module's data models. | | Migrations | The module's migrations, in the order they run. | | Loaders | The module's loaders. | | Service | A function that builds the module's service. It receives the generic MedusaService of the models, which you wrap in your own service. Without it, the module's service is the generic MedusaService. |

The Module function and ModuleDefinition are in the modules-sdk package of utils (github.com/hitkey-io/medusago/packages/core/utils/src/modules-sdk). Loader and ServiceOptions are in the modules-sdk package of types (github.com/hitkey-io/medusago/packages/core/types/src/modules-sdk).

Module returns a *DefinedModule. Its embedded ModuleExports is what you register in the configuration, and its Linkable field is what you use to link the module's data models to other modules.

Warning: Module panics when it starts if a data model is invalid, for example when a relationship points to a model that the module doesn't define.

4. Add Module to MedusaGo's Configurations#

To use the module in your application, add it to 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 config package is github.com/hitkey-io/medusago/packages/core/framework/src/config. The configuration of a module has these fields:

| Field | Description | | --- | --- | | Module | The module's exports: the ModuleExports embedded in the result of Module. | | Resolve | The path to the module's directory, relative to the application's directory. db:generate writes the module's migrations in its migrations directory. | | Key | The key of the module in the container. It's optional, and defaults to the module's name, blog. | | Options | The module's options. |

Limitations: A module of your application can only be registered with Go code, as ModuleConfig.Module isn't part of the JSON and YAML formats. The stock medusa binary, which reads medusa-config.json or medusa-config.yaml, only loads the modules that ship with MedusaGo. Learn more in the Configurations chapter.

5. Generate Migrations#

A migration is a change to the database's schema, with the SQL statements that apply and revert it. To create the tables of the module's data models, you generate a migration with the db:generate command.

MedusaGo's migrations are Go values. The command writes them in a migrations package inside the module, and you reference the list of migrations in the module's definition. So, before you run the command for the first time, create the package with an empty list. Create the file src/modules/blog/migrations/index.go:

src/modules/blog/migrations/index.go
// Package migrations lists the migrations of the module. Create it with an
// empty list before the first db:generate: the command rewrites this file.
package migrations

import (
	m "github.com/hitkey-io/medusago/packages/core/utils/src/migrations"
)

var Migrations = []m.Migration{}

Then, reference it in the module definition with the Migrations field, as in the index.go file of the previous step. Finally, run the following command in your application's directory:

Terminal
go run . db:generate blog

The command needs the application's database, as it connects to it. It logs that the migration was generated, and writes the following files in src/modules/blog/migrations:

  • Migration<timestamp>.go: the migration, with the SQL statements it runs to apply and revert the changes.
  • .snapshot-blog.json: a snapshot of the models' schema. The next time you run the command, it compares your models to the snapshot to find the changes.
  • index.go: it's rewritten to list every migration of the directory.
Note: You can only generate migrations for the modules of your application, whose configuration has a Resolve path. Learn more about migrations in the Migrations chapter.

6. Run Migrations#

To create the module's tables in the database, run the following command:

Terminal
go run . db:migrate

The command runs the pending migrations of every module, syncs the module links, runs the loaders of the modules, and then runs the migration scripts.


Test the Module#

To test the module, resolve its service from the container and use it. The container gives you the service of the module under its key, in a route, a workflow, or a script.

For example, create the following script:

src/scripts/test-blog.go
// TestBlog creates a draft with the blog module and lists the posts.
func TestBlog(ctx context.Context, args typescommon.ExecArgs) error {
	logger := args.Container.Resolve(common.ContainerRegistrationKeys.LOGGER).(*slog.Logger)

	// the module service is registered under the key of the module
	blogService := args.Container.Resolve(blog.Name).(*blog.BlogModuleService)

	if _, err := blogService.CreateDraft(ctx, "My first post", "my-first-post"); err != nil {
		return err
	}

	posts, err := blogService.Internal("Post").List(ctx, dal.NewRecord(), utilsmodulessdk.FindConfig{})
	if err != nil {
		return err
	}
	for _, post := range posts {
		title, _ := post.Get("title")
		status, _ := post.Get("status")
		logger.Info(fmt.Sprintf("%v (%v)", title, status))
	}
	return nil
}

Then, register it by its name in the Scripts option of cli.Run, as explained in the Custom CLI Scripts chapter, and run it:

Terminal
go run . exec test-blog

The script creates a draft with the CreateDraft method of your service, lists the posts with the internal service of the Post data model, and logs them.


Next Steps#

Found a problem with this page?Report Issue
Edited Nov 14·Edit this page