Skip to content

Procedures

Each procedure exposes a Go handler to tRPC clients at a named path. Choose a query, mutation, or subscription, then add middleware, metadata, or output hooks as needed.

Use queries for reads.

type GetUserInput struct {
ID string `json:"id" validate:"required"`
}
func getUser(ctx context.Context, input GetUserInput) (User, error) {
return db.FindUser(input.ID)
}
trpcgo.MustQuery(router, "user.get", getUser)

Use VoidQuery when there is no input:

trpcgo.MustVoidQuery(router, "system.health", func(ctx context.Context) (HealthInfo, error) {
return HealthInfo{OK: true}, nil
})

Use mutations for writes.

trpcgo.MustMutation(router, "user.create", func(ctx context.Context, input CreateUserInput) (User, error) {
return db.CreateUser(input)
})

Use VoidMutation when there is no input:

trpcgo.MustVoidMutation(router, "system.reset", func(ctx context.Context) (ResetResult, error) {
return resetDemoData()
})

Subscriptions return a receive-only channel and send its values as server-sent events (SSE).

trpcgo.MustSubscribe(router, "chat.messages", func(ctx context.Context, input RoomInput) (<-chan Message, error) {
ch := make(chan Message)
go func() {
defer close(ch)
// Send values until ctx is canceled.
}()
return ch, nil
})

Close the channel when the stream finishes, and stop sending when ctx is canceled. See Subscriptions for a complete example with cancellation handling.

Use VoidSubscribe when there is no input. Use SubscribeWithFinal to send a final value in the SSE return event after the channel closes:

trpcgo.MustSubscribeWithFinal(router, "job.progress", func(ctx context.Context, input JobInput) (<-chan Progress, func() any, error) {
ch := runJob(ctx, input)
final := func() any { return map[string]string{"status": "done"} }
return ch, final, nil
})

Here, runJob is your application helper: it returns a progress channel, closes it when the job finishes, and stops work when ctx is canceled.

Non-Must functions return duplicate path errors:

if err := trpcgo.Query(router, "user.get", getUser); err != nil {
return err
}

Must* variants panic and are intended for application startup code where a duplicate path is a programmer mistake.

Every registration function accepts procedure options:

trpcgo.MustMutation(router, "user.create", createUser,
trpcgo.Use(requireAuth, rateLimit),
trpcgo.WithMeta(map[string]string{"action": "write"}),
)

Available procedure options include:

Option Purpose
Use(mw...) Adds per-procedure middleware.
WithMeta(meta) Attaches metadata readable through GetProcedureMeta or GetMeta[T].
WithOutputValidator(fn) Validates successful outputs without changing their type.
OutputValidator[O](fn) Typed output validator.
WithOutputParser(fn) Validates or transforms output, but generated output type becomes unknown.
OutputParser[O, P](fn) Typed output parser; generated output type becomes P.

Procedure() builds immutable, reusable procedure options. Each chain call returns a new builder.

publicProcedure := trpcgo.Procedure()
authedProcedure := publicProcedure.Use(requireAuth)
adminProcedure := authedProcedure.Use(requireAdmin).WithMeta(RoleMeta{Role: "admin"})
trpcgo.MustQuery(router, "user.list", listUsers, authedProcedure)
trpcgo.MustMutation(router, "user.create", createUser, authedProcedure)
trpcgo.MustMutation(router, "admin.ban", banUser, adminProcedure)

Seed one builder from another when composing domains:

orgProcedure := trpcgo.Procedure(authedProcedure).Use(requireOrgAccess)

Use .With(...) to add other options, such as a typed output parser, to a builder:

publicUserProcedure := authedProcedure.With(
trpcgo.OutputParser(func(u User) (PublicUser, error) {
return PublicUser{ID: u.ID, Name: u.Name}, nil
}),
)

Options are applied in order. Middleware accumulates, while later metadata, validators, and parsers replace earlier values of the same kind.

Output hooks run after a handler succeeds. Validators run before parsers.

trpcgo.MustQuery(router, "user.get", getUser,
trpcgo.OutputValidator(func(u User) error {
if u.ID == "" {
return errors.New("id required")
}
return nil
}),
)

Use typed parsers when the client should see a sanitized shape:

type PublicUser struct {
ID string `json:"id"`
Name string `json:"name"`
}
trpcgo.MustQuery(router, "user.get", getUser,
trpcgo.OutputParser(func(u User) (PublicUser, error) {
return PublicUser{ID: u.ID, Name: u.Name}, nil
}),
)

For subscriptions, hooks run on each item and receive the full TrackedEvent[T] wrapper. They also run on non-nil final values. If a hook fails, the server sends an SSE serialized-error event and closes the stream.