在快速开始中,你已经启动了一条链,并提交了一笔交易来增加计数器。在本教程中,你将从零开始构建一个简单的计数器模块。它遵循完整 x/counter 的整体结构,但使用了一个精简版本,这样你可以专注于亲手构建并接入模块的核心步骤。 完成后,你将拥有一个可工作的模块,并将它接入正在运行的链。想更深入了解 Cosmos SDK 中模块的工作方式,请参阅模块简介。
继续之前,你必须先完成前置条件指南,确保所有内容都已安装。

构建模块

Cosmos SDK 让你可以通过模块,直接将自定义业务逻辑构建到链中。每个模块都遵循相同的整体模式:
proto 文件 → 代码生成 → keeper → msg server → query server → module.go → app 装配
首先,你将定义模块的功能:
  • 定义消息:用户可以发送 Add 来增加计数器
  • 定义查询:用户可以查询 Count 来读取当前值
  • 定义创世状态:模块初始计数为 0
然后,你会把这些行为接入 SDK:
  • 运行 proto-gen 生成 Go 类型和接口
  • 在 keeper 中实现业务逻辑,用于存储计数并更新它
  • 实现 MsgServer 和 QueryServer,将消息和查询传递给 keeper
  • 在 module.go 中注册模块
  • 在 app.go 中将其接入链
你将构建如下模块结构:
proto/example/counter/v1/
├── tx.proto            # 交易消息与 Msg 服务定义
├── query.proto         # 查询消息与 Query 服务定义
└── genesis.proto       # 创世状态定义

x/counter/
├── keeper/
│   ├── keeper.go         # Keeper 结构体与状态方法
│   ├── msg_server.go     # MsgServer 实现
│   └── query_server.go   # QueryServer 实现
├── types/
│   ├── keys.go           # 模块名与 store key 常量
│   ├── codec.go          # 接口注册
│   └── *.pb.go           # 由 proto 生成,请勿编辑
├── module.go             # AppModule 装配
└── autocli.go            # CLI 命令定义

步骤 1:准备

本教程使用 tutorial/start 分支,它是一个空白模板,供你从零创建模块并将其接入 app.go。
  1. 如果你还没有克隆仓库,请先执行:
git clone https://github.com/cosmos/example
cd example
  1. 切换到 tutorial/start 分支,并创建新模块目录:
git checkout tutorial/start
mkdir -p x/counter/keeper x/counter/types proto/example/counter/v1
你应该会在 x/counter/ 和 proto/example/counter/v1/ 看到空的占位目录。

步骤 2:Proto 文件

Proto 文件是模块公共 API 的权威来源。你会在这里定义消息和服务。若想更深入了解 protobuf 如何在模块之间使用,请参阅编码与 Protobuf。 在本教程中,counter 模块存储一个数字,Add 会按用户提交的数量增加它,而查询会返回当前值。 首先,创建这三个 proto 文件:
touch proto/example/counter/v1/tx.proto \
  proto/example/counter/v1/query.proto \
  proto/example/counter/v1/genesis.proto
然后,将以下内容添加到每个文件中。

tx.proto

这是你定义的第一个模块文件。它声明了 Add 的交易消息结构:用户发送什么来递增计数器,以及模块在处理后返回什么。若想进一步了解消息的定义与路由方式,请参阅消息。将以下代码添加到 tx.proto。
syntax = "proto3";

// 与模块的 protobuf 命名空间保持一致。
package example.counter;

// 提供 Cosmos SDK 的消息注解,例如 signer 和 service 标记。
import "cosmos/msg/v1/msg.proto";

// 生成的 Go 类型会写入 x/counter/types。
option go_package = "github.com/cosmos/example/x/counter/types";

service Msg {
  // 将其标记为交易服务,而不是普通的 gRPC 服务。
  option (cosmos.msg.v1.service) = true;
  // Add 是这个最小模块支持的唯一交易。
  rpc Add(MsgAddRequest) returns (MsgAddResponse);
}

message MsgAddRequest {
  // 发送者需要为这条消息签名。
  option (cosmos.msg.v1.signer) = "sender";
  string sender = 1;
  uint64 add    = 2;
}

message MsgAddResponse {
  // 在加法成功后返回新的计数器值。
  uint64 updated_count = 1;
}

query.proto

这个文件定义了只读的 gRPC 查询服务,以及用于获取当前计数的响应类型。若想进一步了解查询与交易的区别,请参阅查询。将以下代码添加到 query.proto。
syntax = "proto3";

// 与模块的 protobuf 命名空间保持一致。
package example.counter;

// 启用下面的 REST 网关路由注解。
import "google/api/annotations.proto";

// 生成的 Go 类型会写入 x/counter/types。
option go_package = "github.com/cosmos/example/x/counter/types";

service Query {
  rpc Count(QueryCountRequest) returns (QueryCountResponse) {
    // 将此查询同时暴露到 HTTP API 和 gRPC。
    option (google.api.http).get = "/example/counter/v1/count";
  }
}

// 因为该查询只需要模块的当前状态,所以这里为空。
message QueryCountRequest  {}

message QueryCountResponse {
  // 当前的计数器值。
  uint64 count = 1;
}

genesis.proto

这个文件定义了模块在创世中存储的数据,以便链启动时能够初始化计数器。将以下代码添加到 genesis.proto。
syntax = "proto3";

// 与模块的 protobuf 命名空间保持一致。
package example.counter;

// 生成的 Go 类型会写入 x/counter/types。
option go_package = "github.com/cosmos/example/x/counter/types";

message GenesisState {
  // 链初始化时要加载的计数器值。
  uint64 count = 1;
}

步骤 3:生成代码

  1. 确保 Docker 已在运行。
  2. 第一次运行 proto-gen 时,你需要先构建 builder 镜像。运行以下命令:
make proto-image-build
make proto-gen
这会在 Docker 内使用 buf 编译 proto 文件,生成后续需要由你实现的 Go 接口。 生成的文件会出现在 x/counter/types/:
x/counter/types/
├── tx.pb.go         # MsgAddRequest、MsgAddResponse、MsgServer 接口
├── query.pb.go      # QueryCountRequest、QueryCountResponse、QueryServer 接口
├── query.pb.gw.go   # REST 网关注册
└── genesis.pb.go    # GenesisState
不要编辑生成文件。 公共类型的变更应当修改 proto 文件。每次修改 proto 后,都重新运行 make proto-gen。
最重要的生成结果是 MsgServer 和 QueryServer 接口。在步骤 5 和 6 中,你会分别在 keeper/msg_server.go 和 keeper/query_server.go 中实现它们。

步骤 4:Types

接下来,你将在 x/counter/types 中定义模块类型和标识符,供模块其余部分依赖。 为本节创建这两个文件:
touch x/counter/types/keys.go \
  x/counter/types/codec.go
然后,将以下内容添加到每个文件中。

keys.go

这个文件定义了模块的基础标识符:一个是在整个 SDK 中使用的模块名,另一个是用于声明该模块 KV store 命名空间的 store key。想进一步了解模块如何通过 store key 访问状态,请参阅模块如何访问状态。
// x/counter/types/keys.go
package types

const (
    // ModuleName 是 SDK 用来引用该模块的名称。
    ModuleName = "counter"
    // StoreKey 是该模块 KV store 的键。
    StoreKey   = ModuleName
)
ModuleName 在整个 SDK 中标识该模块(路由、事件、治理)。StoreKey 是模块在链的 KV store 中声明其隔离命名空间所使用的键(按约定它等于 ModuleName)。

接口注册

这个文件会将你生成的消息类型注册到 SDK 的接口注册表中,以便应用能够正确解码并路由你模块的交易。
// x/counter/types/codec.go
package types

import (
    codectypes "github.com/cosmos/cosmos-sdk/codec/types"
    sdk        "github.com/cosmos/cosmos-sdk/types"
    "github.com/cosmos/cosmos-sdk/types/msgservice"
)

func RegisterInterfaces(registry codectypes.InterfaceRegistry) {
    // 将 MsgAddRequest 注册为 sdk.Msg,使应用能够从交易中解码它。
    registry.RegisterImplementations((*sdk.Msg)(nil),
        &MsgAddRequest{},
    )
    // 注册生成的 Msg 服务描述,用于路由。
    msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)
}
_Msg_serviceDesc 由 make proto-gen 生成,它描述了在 tx.proto 中定义的 Msg gRPC 服务。

第 5 步:Keeper

在这一步中,你将创建 keeper。它是模块中负责持有计数器状态并提供模块其他部分调用方法的组件。关于 keeper 角色的概念性概览,参见 Keeper。 创建 keeper 文件:
touch x/counter/keeper/keeper.go
然后添加以下内容。 这个文件定义了 keeper 结构体,设置了计数器的存储项,并实现了在创世阶段读取、更新和加载计数器的核心状态方法。
// x/counter/keeper/keeper.go
package keeper

import (
    "context"
    "errors"

    "cosmossdk.io/collections"
    "cosmossdk.io/core/store"
    "github.com/cosmos/cosmos-sdk/codec"
    "github.com/cosmos/example/x/counter/types"
)

type Keeper struct {
    Schema  collections.Schema
    counter collections.Item[uint64]
}

func NewKeeper(storeService store.KVStoreService, cdc codec.Codec) *Keeper {
    sb := collections.NewSchemaBuilder(storeService)
    k := Keeper{
        // Store the counter under prefix 0 in this module's KV store.
        counter: collections.NewItem(sb, collections.NewPrefix(0), "counter", collections.Uint64Value),
    }
    schema, err := sb.Build()
    if err != nil {
        panic(err)
    }
    k.Schema = schema
    return &k
}

func (k *Keeper) GetCount(ctx context.Context) (uint64, error) {
    count, err := k.counter.Get(ctx)
    // Treat missing state as zero so a fresh chain starts cleanly.
    if err != nil && !errors.Is(err, collections.ErrNotFound) {
        return 0, err
    }
    return count, nil
}

func (k *Keeper) AddCount(ctx context.Context, amount uint64) (uint64, error) {
    count, err := k.GetCount(ctx)
    if err != nil {
        return 0, err
    }
    // Increment the current count and write it back to state.
    newCount := count + amount
    return newCount, k.counter.Set(ctx, newCount)
}

func (k *Keeper) InitGenesis(ctx context.Context, gs *types.GenesisState) error {
    return k.counter.Set(ctx, gs.Count)
}

func (k *Keeper) ExportGenesis(ctx context.Context) (*types.GenesisState, error) {
    count, err := k.GetCount(ctx)
    if err != nil {
        return nil, err
    }
    return &types.GenesisState{Count: count}, nil
}
collections.Item[uint64] 是一个带类型的 KV 存储条目;collections 包负责处理编码和命名空间。GetCount 将 ErrNotFound 视为零,因此计数器在没有显式初始化的情况下也会从零开始。
状态布局
  • StoreKey("counter")是该模块在链全局 KV 存储中的独立命名空间。其他模块都不能读取或写入这个命名空间。
  • collections.NewPrefix(0) 是一个单字节前缀,用于在模块命名空间内标识 counter 这一项。若模块包含多个存储项,通常会使用 NewPrefix(0)、NewPrefix(1) 等将它们彼此分隔。
  • 将 ErrNotFound 视为零,意味着 keeper 无需显式设置初始值;在一条全新链上,第一次调用 GetCount 时按约定会返回 0。

第 6 步:MsgServer

在这一步中,你将为生成出来的 MsgServer 接口实现交易处理器。当用户提交 tx counter add 时,就会执行这段代码路径。关于消息执行的概念性概览,参见 Message execution。 创建消息服务器文件:
touch x/counter/keeper/msg_server.go
然后添加以下内容。 这个文件实现了生成的 MsgServer 接口,并将 Add 交易转发给 keeper 的 AddCount 方法。
// x/counter/keeper/msg_server.go
package keeper

import (
    "context"

    "github.com/cosmos/example/x/counter/types"
)

type msgServer struct {
    *Keeper
}

func NewMsgServerImpl(k *Keeper) types.MsgServer {
    return &msgServer{k}
}

func (m msgServer) Add(ctx context.Context, req *types.MsgAddRequest) (*types.MsgAddResponse, error) {
    // Delegate the state update to the keeper.
    newCount, err := m.AddCount(ctx, req.GetAdd())
    if err != nil {
        return nil, err
    }
    // Return the updated count back to the caller.
    return &types.MsgAddResponse{UpdatedCount: newCount}, nil
}
msgServer 内嵌了 *Keeper,并直接委托给 AddCount。处理器本身不包含任何业务逻辑。

第 7 步:QueryServer

在这一步中,你将为生成出来的 QueryServer 接口实现只读查询处理器。当有人查询当前计数器值时,就会执行这段代码路径。关于模块如何暴露查询的更多内容,参见 Queries。 创建查询服务器文件:
touch x/counter/keeper/query_server.go
然后添加以下内容。 这个文件实现了生成的 QueryServer 接口,并从 keeper 返回当前计数器值。
// x/counter/keeper/query_server.go
package keeper

import (
    "context"

    "github.com/cosmos/example/x/counter/types"
)

type queryServer struct {
    *Keeper
}

func NewQueryServer(k *Keeper) types.QueryServer {
    return &queryServer{k}
}

func (q queryServer) Count(ctx context.Context, _ *types.QueryCountRequest) (*types.QueryCountResponse, error) {
    // Read the current count from state and return it in the query response.
    count, err := q.GetCount(ctx)
    if err != nil {
        return nil, err
    }
    return &types.QueryCountResponse{Count: count}, nil
}

第 8 步:module.go

在这一步中,你将把 keeper 和生成出的服务连接到 Cosmos SDK 模块框架中,这样应用就知道如何初始化模块、暴露其查询路由,以及注册其交易处理器。 创建模块文件:
touch x/counter/module.go
然后添加以下内容。 这个文件定义了应用模块类型,并将 keeper 接入创世处理、服务注册和 gRPC 网关注册。
// x/counter/module.go
package counter

import (
    "context"
    "encoding/json"

    "cosmossdk.io/core/appmodule"
    "github.com/cosmos/cosmos-sdk/client"
    "github.com/cosmos/cosmos-sdk/codec"
    codecTypes "github.com/cosmos/cosmos-sdk/codec/types"
    sdk "github.com/cosmos/cosmos-sdk/types"
    "github.com/cosmos/cosmos-sdk/types/module"
    "github.com/grpc-ecosystem/grpc-gateway/runtime"

    "github.com/cosmos/example/x/counter/keeper"
    countertypes "github.com/cosmos/example/x/counter/types"
)

var (
    // Compile-time checks that AppModule implements the required module interfaces.
    _ appmodule.AppModule        = AppModule{}
    _ module.HasConsensusVersion = AppModule{}
    _ module.HasGenesis          = AppModule{}
    _ module.HasServices         = AppModule{}
)

type AppModuleBasic struct {
    cdc codec.Codec
}

func (a AppModuleBasic) Name() string { return countertypes.ModuleName }

func (a AppModuleBasic) RegisterLegacyAminoCodec(*codec.LegacyAmino) {}

func (a AppModuleBasic) RegisterInterfaces(registry codecTypes.InterfaceRegistry) {
    countertypes.RegisterInterfaces(registry)
}

func (a AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage {
    // Start the module with a zero counter by default.
    return cdc.MustMarshalJSON(&countertypes.GenesisState{Count: 0})
}

func (a AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, _ client.TxEncodingConfig, bz json.RawMessage) error {
    gs := countertypes.GenesisState{}
    return cdc.UnmarshalJSON(bz, &gs)
}

func (a AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) {
    // Expose the Query service through the HTTP gateway.
    if err := countertypes.RegisterQueryHandlerClient(context.Background(), mux, countertypes.NewQueryClient(clientCtx)); err != nil {
        panic(err)
    }
}

type AppModule struct {
    AppModuleBasic
    keeper *keeper.Keeper
}

func NewAppModule(cdc codec.Codec, k *keeper.Keeper) AppModule {
    return AppModule{AppModuleBasic: AppModuleBasic{cdc: cdc}, keeper: k}
}

func (a AppModule) IsOnePerModuleType() {}
func (a AppModule) IsAppModule()        {}

func (a AppModule) ConsensusVersion() uint64 { return 1 }

func (a AppModule) RegisterServices(cfg module.Configurator) {
    // Connect the generated service interfaces to your keeper-backed implementations.
    countertypes.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(a.keeper))
    countertypes.RegisterQueryServer(cfg.QueryServer(), keeper.NewQueryServer(a.keeper))
}

func (a AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, bz json.RawMessage) {
    gs := &countertypes.GenesisState{}
    cdc.MustUnmarshalJSON(bz, gs)
    // Load the initial counter value into state at chain start.
    if err := a.keeper.InitGenesis(ctx, gs); err != nil {
        panic(err)
    }
}

func (a AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage {
    gs, err := a.keeper.ExportGenesis(ctx)
    if err != nil {
        panic(err)
    }
    // Write the current counter value back out for exports.
    return cdc.MustMarshalJSON(gs)
}
顶部的 var _ interface = Struct{} 代码块是 Go 的编译期检查机制:如果结构体缺少任何必需的方法,构建会立即失败。 RegisterServices 是最重要的方法。它会将生成的服务端接口连接到你的实现上,使其可以通过 SDK 的消息路由器和查询路由器访问。

第 9 步:AutoCLI

在这一步中,你将为模块定义 CLI 元数据。AutoCLI 会结合这份配置和你的 proto 服务,自动生成 exampled query counter 和 exampled tx counter 命令。 创建 AutoCLI 文件:
touch x/counter/autocli.go
然后添加以下内容。 这个文件告诉 AutoCLI 如何将 Count 查询和 Add 交易暴露为简单的命令行命令。
// x/counter/autocli.go
package counter

import (
    autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
)

func (a AppModule) AutoCLIOptions() *autocliv1.ModuleOptions {
    return &autocliv1.ModuleOptions{
        Query: &autocliv1.ServiceCommandDescriptor{
            Service: "example.counter.Query",
            RpcCommandOptions: []*autocliv1.RpcCommandOptions{
                // exampled query counter count
                {RpcMethod: "Count", Use: "count", Short: "Query the current counter value"},
            },
        },
        Tx: &autocliv1.ServiceCommandDescriptor{
            Service: "example.counter.Msg",
            RpcCommandOptions: []*autocliv1.RpcCommandOptions{
                // exampled tx counter add 4 --from alice
                {RpcMethod: "Add", Use: "add [amount]", Short: "Add to the counter",
                    PositionalArgs: []*autocliv1.PositionalArgDescriptor{{ProtoField: "add"}}},
            },
        },
    }
}
PositionalArgs 会将第一个 CLI 参数映射到 MsgAddRequest 中的 add 字段,因此可以使用 add 4,而不是 add --add 4。

第 10 步:接入 app.go

在这一步中,你将把新模块接入应用,使链能够创建其存储、构造其 keeper,并在模块启动和创世处理时将其纳入其中。关于 app.go 的作用以及接入顺序为何重要的完整说明,参见 app.go Overview。 打开 app.go 并找到每个标记注释。将代码直接粘贴到注释下方。

1. 导入

将 counter 模块、keeper 和共享类型的导入添加到 app.go。 在 app.go 中找到对应注释,并将代码直接添加到它下方。
// counter tutorial app wiring 1: add counter imports below
	counter       "github.com/cosmos/example/x/counter"
	counterkeeper "github.com/cosmos/example/x/counter/keeper"
	countertypes  "github.com/cosmos/example/x/counter/types"

2. Keeper 字段

将计数器 keeper 存储在 ExampleApp 上,以便应用其余部分可以引用它。
// counter tutorial app wiring 2: add the counter keeper field below
CounterKeeper         *counterkeeper.Keeper

3. Store Key

为计数器模块提供它自己的 KV 存储命名空间。
// counter tutorial app wiring 3: add the counter store key below
countertypes.StoreKey,

4. Keeper 实例化

使用模块存储和应用 codec 构造计数器 keeper。
// counter tutorial app wiring 4: create the counter keeper below
app.CounterKeeper = counterkeeper.NewKeeper(
	runtime.NewKVStoreService(keys[countertypes.StoreKey]),
	appCodec,
)

5. 模块管理器

将计数器模块注册到应用的模块管理器中。
// counter tutorial app wiring 5: register the counter module below
counter.NewAppModule(appCodec, app.CounterKeeper),

6. Genesis 顺序

在应用从 genesis 初始化状态时包含计数器模块。
// counter tutorial app wiring 6: add the counter module to genesis order below
countertypes.ModuleName,

7. 导出顺序

在应用将状态导出回 genesis 时包含计数器模块。
// counter tutorial app wiring 7: add the counter module to export order below
countertypes.ModuleName,

第 11 步:构建

运行以下命令来编译应用,并在尝试运行链之前确认新的模块接线是有效的。
go build ./...
继续之前,先修复所有编译错误。

第 12 步:测试你的模块

现在你将在本地运行应用,并通过一笔交易加一次查询来确认该模块可以端到端正常工作。

启动链

首先,安装二进制并启动演示链。
make install
make start
这会构建并安装 exampled,然后运行 scripts/local_node.sh,该脚本会:
  • 重置本地链数据
  • 初始化 genesis
  • 创建并为 alice 和 bob 测试账户注资
  • 创建一笔验证者交易
  • 启动链
你会看到链开始运行,并且它应该会开始出块。

提交一笔交易

打开第二个终端,并提交一笔向计数器增加 4 的交易:
exampled tx counter add 4 --from alice --chain-id demo --yes
如果交易成功,响应中应包含 code: 0,这表示链已接受并执行该交易,且没有发生应用错误:
code: 0

查询链

使用之前由 AutoCLI 生成的查询命令查询计数器,以确认已存储的值发生了变化:
exampled query counter count
你应该会看到以下输出:
count: "4"
恭喜,你刚刚从零开始创建了一个 Cosmos 模块,并将它接入了一条真实的链。 如果你打算构建一个生产级模块,请参阅模块设计注意事项,了解在发布前关于状态结构、消息接口、依赖关系和升级规划的指导。

后续步骤

你在这里构建的简单计数器模块,遵循了 main 分支中完整 x/counter 示例相同的结构。接下来,你将看到完整模块如何在这个基础上扩展出参数、费用收集、测试等功能。 下一步:完整 Counter 模块演练 →
In quickstart, you started a chain and submitted a transaction to increase the counter. In this tutorial, you’ll build a simple counter module from scratch. It follows the same overall structure as the full x/counter, but uses a stripped-down version so you can focus on the core steps of building and wiring a module yourself. By the end, you’ll have built a working module and wired it into a running chain. For a deeper dive into how modules work in the Cosmos SDK, see Intro to Modules.
Before continuing, you must follow the Prerequisites guide to make sure everything is installed.

Making modules

The Cosmos SDK makes it easy to build custom business logic directly into your chain through modules. Every module follows the same overall pattern:
proto files → code generation → keeper → msg server → query server → module.go → app wiring
First, you’ll define what the module does:
  • Define messages: users can send Add to increase the counter
  • Define queries: users can query Count to read the current value
  • Define genesis state: the module starts with a count of 0
Then you’ll wire that behavior into the SDK:
  • Run proto-gen to generate the Go types and interfaces
  • Implement your business logic in a keeper to store the count and update it
  • Implement MsgServer and QueryServer to pass messages and queries into the keeper
  • Register the module in module.go
  • Wire it into the chain in app.go
You’ll build the following module structure:
proto/example/counter/v1/
├── tx.proto            # Transaction message and Msg service definition
├── query.proto         # Query message and Query service definition
└── genesis.proto       # Genesis state definition

x/counter/
├── keeper/
│   ├── keeper.go         # Keeper struct and state methods
│   ├── msg_server.go     # MsgServer implementation
│   └── query_server.go   # QueryServer implementation
├── types/
│   ├── keys.go           # Module name and store key constants
│   ├── codec.go          # Interface registration
│   └── *.pb.go           # Generated from proto — do not edit
├── module.go             # AppModule wiring
└── autocli.go            # CLI command definitions

Step 1: Setup

This tutorial uses the tutorial/start branch, which is a blank template for you to create the module from scratch and wire it into app.go.
  1. Clone the repo if you haven’t already:
git clone https://github.com/cosmos/example
cd example
  1. Check out the tutorial/start branch and make the new module directories:
git checkout tutorial/start
mkdir -p x/counter/keeper x/counter/types proto/example/counter/v1
You should see empty placeholder directories at x/counter/ and proto/example/counter/v1/.

Step 2: Proto files

Proto files are the source of truth for the module’s public API. You define messages and services here. For a deeper look at how protobuf is used across modules, see Encoding and Protobuf. In this tutorial, the counter module stores one number, Add increases it by the amount the user submits, and the query returns the current value. First, create the three proto files:
touch proto/example/counter/v1/tx.proto \
  proto/example/counter/v1/query.proto \
  proto/example/counter/v1/genesis.proto
Then add the following contents to each file.

tx.proto

This is the first module file you define. It declares the transaction message shape for Add: what the user sends to increment the counter, and what the module returns after handling it. To learn more about how messages are defined and routed, see Messages. Add the following code to tx.proto.
syntax = "proto3";

// Matches the module's protobuf namespace.
package example.counter;

// Provides Cosmos SDK message annotations like signer and service markers.
import "cosmos/msg/v1/msg.proto";

// Generated Go types are written into x/counter/types.
option go_package = "github.com/cosmos/example/x/counter/types";

service Msg {
  // Marks this as a transaction service, not a normal gRPC service.
  option (cosmos.msg.v1.service) = true;
  // Add is the one transaction this minimal module supports.
  rpc Add(MsgAddRequest) returns (MsgAddResponse);
}

message MsgAddRequest {
  // The sender signs this message.
  option (cosmos.msg.v1.signer) = "sender";
  string sender = 1;
  uint64 add    = 2;
}

message MsgAddResponse {
  // Return the new counter value after the add succeeds.
  uint64 updated_count = 1;
}

query.proto

This file defines the read-only gRPC query service and the response type for fetching the current count. To learn more about how queries differ from transactions, see Queries. Add the following code to query.proto.
syntax = "proto3";

// Matches the module's protobuf namespace.
package example.counter;

// Enables the REST gateway route annotation below.
import "google/api/annotations.proto";

// Generated Go types are written into x/counter/types.
option go_package = "github.com/cosmos/example/x/counter/types";

service Query {
  rpc Count(QueryCountRequest) returns (QueryCountResponse) {
    // Exposes this query over the HTTP API as well as gRPC.
    option (google.api.http).get = "/example/counter/v1/count";
  }
}

// Empty because this query only needs the module's current state.
message QueryCountRequest  {}

message QueryCountResponse {
  // The current counter value.
  uint64 count = 1;
}

genesis.proto

This file defines the data the module stores in genesis so the counter can be initialized when the chain starts. Add the following code to genesis.proto.
syntax = "proto3";

// Matches the module's protobuf namespace.
package example.counter;

// Generated Go types are written into x/counter/types.
option go_package = "github.com/cosmos/example/x/counter/types";

message GenesisState {
  // The counter value to load when the chain initializes.
  uint64 count = 1;
}

Step 3: Generate Code

  1. Make sure Docker is running.
  2. The first time you run proto-gen you need to build the builder image. Run the following commands:
make proto-image-build
make proto-gen
This compiles the proto files using buf inside Docker to produce the Go interfaces you will then implement. The generated files will appear in x/counter/types/:
x/counter/types/
├── tx.pb.go         # MsgAddRequest, MsgAddResponse, MsgServer interface
├── query.pb.go      # QueryCountRequest, QueryCountResponse, QueryServer interface
├── query.pb.gw.go   # REST gateway registration
└── genesis.pb.go    # GenesisState
Do not edit generated files. Changes to public types belong in the proto files. Re-run make proto-gen after any proto change.
The most important generated output is the MsgServer and QueryServer interfaces. In Steps 5 and 6, you’ll implement them in keeper/msg_server.go and keeper/query_server.go.

Step 4: Types

Next, you’ll define the module types and identifiers in x/counter/types that the rest of the module depends on. Create the two files for this section:
touch x/counter/types/keys.go \
  x/counter/types/codec.go
Then add the following contents to each file.

keys.go

This file defines the module’s basic identifiers: the module name used throughout the SDK, and the store key used to claim the module’s KV store namespace. For more on how modules access state through store keys, see How modules access state.
// x/counter/types/keys.go
package types

const (
    // ModuleName is the name the SDK uses to refer to this module.
    ModuleName = "counter"
    // StoreKey is the key for this module's KV store.
    StoreKey   = ModuleName
)
ModuleName identifies the module throughout the SDK (routing, events, governance). StoreKey is the key used to claim the module’s isolated namespace in the chain’s KV store (set equal to ModuleName by convention).

Interface Registration

This file registers your generated message types with the SDK interface registry so the application can decode and route your module’s transactions correctly.
// x/counter/types/codec.go
package types

import (
    codectypes "github.com/cosmos/cosmos-sdk/codec/types"
    sdk        "github.com/cosmos/cosmos-sdk/types"
    "github.com/cosmos/cosmos-sdk/types/msgservice"
)

func RegisterInterfaces(registry codectypes.InterfaceRegistry) {
    // Register MsgAddRequest as an sdk.Msg so the app can decode it from transactions.
    registry.RegisterImplementations((*sdk.Msg)(nil),
        &MsgAddRequest{},
    )
    // Register the generated Msg service description for routing.
    msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)
}
_Msg_serviceDesc is generated by make proto-gen — it describes the Msg gRPC service defined in tx.proto.

Step 5: Keeper

In this step, you create the keeper, which is the part of the module that owns the counter state and provides the methods the rest of the module will call. For a conceptual overview of the keeper’s role, see Keeper. Create the keeper file:
touch x/counter/keeper/keeper.go
Then add the following contents. This file defines the keeper struct, sets up the counter’s storage item, and implements the core state methods for reading, updating, and loading the counter at genesis.
// x/counter/keeper/keeper.go
package keeper

import (
    "context"
    "errors"

    "cosmossdk.io/collections"
    "cosmossdk.io/core/store"
    "github.com/cosmos/cosmos-sdk/codec"
    "github.com/cosmos/example/x/counter/types"
)

type Keeper struct {
    Schema  collections.Schema
    counter collections.Item[uint64]
}

func NewKeeper(storeService store.KVStoreService, cdc codec.Codec) *Keeper {
    sb := collections.NewSchemaBuilder(storeService)
    k := Keeper{
        // Store the counter under prefix 0 in this module's KV store.
        counter: collections.NewItem(sb, collections.NewPrefix(0), "counter", collections.Uint64Value),
    }
    schema, err := sb.Build()
    if err != nil {
        panic(err)
    }
    k.Schema = schema
    return &k
}

func (k *Keeper) GetCount(ctx context.Context) (uint64, error) {
    count, err := k.counter.Get(ctx)
    // Treat missing state as zero so a fresh chain starts cleanly.
    if err != nil && !errors.Is(err, collections.ErrNotFound) {
        return 0, err
    }
    return count, nil
}

func (k *Keeper) AddCount(ctx context.Context, amount uint64) (uint64, error) {
    count, err := k.GetCount(ctx)
    if err != nil {
        return 0, err
    }
    // Increment the current count and write it back to state.
    newCount := count + amount
    return newCount, k.counter.Set(ctx, newCount)
}

func (k *Keeper) InitGenesis(ctx context.Context, gs *types.GenesisState) error {
    return k.counter.Set(ctx, gs.Count)
}

func (k *Keeper) ExportGenesis(ctx context.Context) (*types.GenesisState, error) {
    count, err := k.GetCount(ctx)
    if err != nil {
        return nil, err
    }
    return &types.GenesisState{Count: count}, nil
}
collections.Item[uint64] is a typed KV store entry; the collections package handles encoding and namespacing. GetCount treats ErrNotFound as zero so the counter starts at zero without explicit initialization.
State layout
  • StoreKey ("counter") is the module’s isolated namespace within the chain’s global KV store. No other module can read or write this namespace.
  • collections.NewPrefix(0) is a single-byte prefix that identifies the counter item within the module’s namespace. A module with multiple items would use NewPrefix(0), NewPrefix(1), etc. to keep them separate.
  • ErrNotFound treated as zero means the keeper never needs to explicitly set an initial value — the first GetCount call on a fresh chain returns 0 by convention.

Step 6: MsgServer

In this step, you implement the transaction handler for the generated MsgServer interface. This is the code path that runs when a user submits tx counter add. For a conceptual overview of message execution, see Message execution. Create the message server file:
touch x/counter/keeper/msg_server.go
Then add the following contents. This file implements the generated MsgServer interface and forwards the Add transaction to the keeper’s AddCount method.
// x/counter/keeper/msg_server.go
package keeper

import (
    "context"

    "github.com/cosmos/example/x/counter/types"
)

type msgServer struct {
    *Keeper
}

func NewMsgServerImpl(k *Keeper) types.MsgServer {
    return &msgServer{k}
}

func (m msgServer) Add(ctx context.Context, req *types.MsgAddRequest) (*types.MsgAddResponse, error) {
    // Delegate the state update to the keeper.
    newCount, err := m.AddCount(ctx, req.GetAdd())
    if err != nil {
        return nil, err
    }
    // Return the updated count back to the caller.
    return &types.MsgAddResponse{UpdatedCount: newCount}, nil
}
msgServer embeds *Keeper and delegates directly to AddCount. The handler itself contains no business logic.

Step 7: QueryServer

In this step, you implement the read-only query handler for the generated QueryServer interface. This is the code path that runs when someone queries the current counter value. For more on how modules expose queries, see Queries. Create the query server file:
touch x/counter/keeper/query_server.go
Then add the following contents. This file implements the generated QueryServer interface and returns the current counter value from the keeper.
// x/counter/keeper/query_server.go
package keeper

import (
    "context"

    "github.com/cosmos/example/x/counter/types"
)

type queryServer struct {
    *Keeper
}

func NewQueryServer(k *Keeper) types.QueryServer {
    return &queryServer{k}
}

func (q queryServer) Count(ctx context.Context, _ *types.QueryCountRequest) (*types.QueryCountResponse, error) {
    // Read the current count from state and return it in the query response.
    count, err := q.GetCount(ctx)
    if err != nil {
        return nil, err
    }
    return &types.QueryCountResponse{Count: count}, nil
}

Step 8: module.go

In this step, you connect your keeper and generated services to the Cosmos SDK module framework so the application knows how to initialize the module, expose its query routes, and register its transaction handlers. Create the module file:
touch x/counter/module.go
Then add the following contents. This file defines the app module types and wires your keeper into genesis handling, service registration, and gRPC gateway registration.
// x/counter/module.go
package counter

import (
    "context"
    "encoding/json"

    "cosmossdk.io/core/appmodule"
    "github.com/cosmos/cosmos-sdk/client"
    "github.com/cosmos/cosmos-sdk/codec"
    codecTypes "github.com/cosmos/cosmos-sdk/codec/types"
    sdk "github.com/cosmos/cosmos-sdk/types"
    "github.com/cosmos/cosmos-sdk/types/module"
    "github.com/grpc-ecosystem/grpc-gateway/runtime"

    "github.com/cosmos/example/x/counter/keeper"
    countertypes "github.com/cosmos/example/x/counter/types"
)

var (
    // Compile-time checks that AppModule implements the required module interfaces.
    _ appmodule.AppModule        = AppModule{}
    _ module.HasConsensusVersion = AppModule{}
    _ module.HasGenesis          = AppModule{}
    _ module.HasServices         = AppModule{}
)

type AppModuleBasic struct {
    cdc codec.Codec
}

func (a AppModuleBasic) Name() string { return countertypes.ModuleName }

func (a AppModuleBasic) RegisterLegacyAminoCodec(*codec.LegacyAmino) {}

func (a AppModuleBasic) RegisterInterfaces(registry codecTypes.InterfaceRegistry) {
    countertypes.RegisterInterfaces(registry)
}

func (a AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage {
    // Start the module with a zero counter by default.
    return cdc.MustMarshalJSON(&countertypes.GenesisState{Count: 0})
}

func (a AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, _ client.TxEncodingConfig, bz json.RawMessage) error {
    gs := countertypes.GenesisState{}
    return cdc.UnmarshalJSON(bz, &gs)
}

func (a AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) {
    // Expose the Query service through the HTTP gateway.
    if err := countertypes.RegisterQueryHandlerClient(context.Background(), mux, countertypes.NewQueryClient(clientCtx)); err != nil {
        panic(err)
    }
}

type AppModule struct {
    AppModuleBasic
    keeper *keeper.Keeper
}

func NewAppModule(cdc codec.Codec, k *keeper.Keeper) AppModule {
    return AppModule{AppModuleBasic: AppModuleBasic{cdc: cdc}, keeper: k}
}

func (a AppModule) IsOnePerModuleType() {}
func (a AppModule) IsAppModule()        {}

func (a AppModule) ConsensusVersion() uint64 { return 1 }

func (a AppModule) RegisterServices(cfg module.Configurator) {
    // Connect the generated service interfaces to your keeper-backed implementations.
    countertypes.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(a.keeper))
    countertypes.RegisterQueryServer(cfg.QueryServer(), keeper.NewQueryServer(a.keeper))
}

func (a AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, bz json.RawMessage) {
    gs := &countertypes.GenesisState{}
    cdc.MustUnmarshalJSON(bz, gs)
    // Load the initial counter value into state at chain start.
    if err := a.keeper.InitGenesis(ctx, gs); err != nil {
        panic(err)
    }
}

func (a AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage {
    gs, err := a.keeper.ExportGenesis(ctx)
    if err != nil {
        panic(err)
    }
    // Write the current counter value back out for exports.
    return cdc.MustMarshalJSON(gs)
}
The var _ interface = Struct{} block at the top is a Go compile-time check — if the struct is missing any required method, the build fails immediately. RegisterServices is the most important method. It connects the generated server interfaces to your implementations, making them reachable from the SDK’s message and query routers.

Step 9: AutoCLI

In this step, you define the CLI metadata for your module. AutoCLI reads this configuration together with your proto services and generates the exampled query counter and exampled tx counter commands automatically. Create the AutoCLI file:
touch x/counter/autocli.go
Then add the following contents. This file tells AutoCLI how to expose the Count query and Add transaction as simple command-line commands.
// x/counter/autocli.go
package counter

import (
    autocliv1 "cosmossdk.io/api/cosmos/autocli/v1"
)

func (a AppModule) AutoCLIOptions() *autocliv1.ModuleOptions {
    return &autocliv1.ModuleOptions{
        Query: &autocliv1.ServiceCommandDescriptor{
            Service: "example.counter.Query",
            RpcCommandOptions: []*autocliv1.RpcCommandOptions{
                // exampled query counter count
                {RpcMethod: "Count", Use: "count", Short: "Query the current counter value"},
            },
        },
        Tx: &autocliv1.ServiceCommandDescriptor{
            Service: "example.counter.Msg",
            RpcCommandOptions: []*autocliv1.RpcCommandOptions{
                // exampled tx counter add 4 --from alice
                {RpcMethod: "Add", Use: "add [amount]", Short: "Add to the counter",
                    PositionalArgs: []*autocliv1.PositionalArgDescriptor{{ProtoField: "add"}}},
            },
        },
    }
}
PositionalArgs maps the first CLI argument to the add field in MsgAddRequest, so add 4 works instead of add --add 4.

Step 10: Wire into app.go

In this step, you wire your new module into the application so the chain creates its store, constructs its keeper, and includes it in module startup and genesis handling. For a full explanation of what app.go does and why the wiring order matters, see app.go Overview. Open app.go and find each marker comment. Paste the code directly below it.

1. Imports

Add the counter module, keeper, and shared types imports to app.go. Find the comment in app.go and add the code directly below it.
// counter tutorial app wiring 1: add counter imports below
	counter       "github.com/cosmos/example/x/counter"
	counterkeeper "github.com/cosmos/example/x/counter/keeper"
	countertypes  "github.com/cosmos/example/x/counter/types"

2. Keeper Field

Store the counter keeper on ExampleApp so the rest of the app can reference it.
// counter tutorial app wiring 2: add the counter keeper field below
CounterKeeper         *counterkeeper.Keeper

3. Store Key

Give the counter module its own KV store namespace.
// counter tutorial app wiring 3: add the counter store key below
countertypes.StoreKey,

4. Keeper Instantiation

Construct the counter keeper using the module store and app codec.
// counter tutorial app wiring 4: create the counter keeper below
app.CounterKeeper = counterkeeper.NewKeeper(
	runtime.NewKVStoreService(keys[countertypes.StoreKey]),
	appCodec,
)

5. Module Manager

Register the counter module with the app’s module manager.
// counter tutorial app wiring 5: register the counter module below
counter.NewAppModule(appCodec, app.CounterKeeper),

6. Genesis Order

Include the counter module when the app initializes state from genesis.
// counter tutorial app wiring 6: add the counter module to genesis order below
countertypes.ModuleName,

7. Export Order

Include the counter module when the app exports state back out to genesis.
// counter tutorial app wiring 7: add the counter module to export order below
countertypes.ModuleName,

Step 11: Build

Run the following to compile the app and make sure the new module wiring is valid before you try to run the chain.
go build ./...
Fix any compilation errors before continuing.

Step 12: Test your module

Now you’ll run the app locally and use one transaction plus one query to confirm the module works end-to-end.

Start the chain

First, install the binary and start the demo chain.
make install
make start
This builds and installs exampled and then runs scripts/local_node.sh, which:
  • resets the local chain data
  • initializes genesis
  • creates and funds the alice and bob test accounts
  • creates a validator transaction
  • starts the chain
You’ll see the chain running and it should start producing blocks.

Submit a transaction

Open a second terminal and submit a transaction that adds 4 to the counter:
exampled tx counter add 4 --from alice --chain-id demo --yes
If the transaction succeeds, the response should include code: 0, which means the chain accepted and executed the transaction without an application error:
code: 0

Query the chain

Query the counter to confirm the stored value changed using the query command that AutoCLI generated earlier:
exampled query counter count
You should see the following output:
count: "4"
Congratulations, you’ve just created a Cosmos module from scratch and wired it into a real chain! If you are planning to build a production module, see Module Design Considerations for guidance on state structure, message surface, dependencies, and upgrade planning before you ship.

Next steps

The simple counter module you built here follows the same structure as the full x/counter example in the main branch. Next, you’ll see how the full module extends that foundation with features like params, fee collection, tests, and more. Next: Full Counter Module Walkthrough →