Create a new GRPC (Connect RPC) handler for the Order Service following established project patterns. Covers handler file, server registration, store layer, converter, and mock updates.
You are creating a new Connect RPC handler for the Order Service. Follow every step below precisely — the project has strict conventions.
Before writing any code, determine:
UpdateOrder, DeleteOrder, CancelOrder)If the user hasn't specified these, ask before proceeding.
File: internal/domain/orders/grpc_{method_name_snake}_handler.go
Use this exact structure (example for UpdateOrder):
package orders
import (
"context"
"connectrpc.com/connect"
orderv1 "github.com/cooli88/contracts2/gen/go/order/v1"
"github.com/cooli88/order2/internal/store"
)
type updateOrderHandler struct {
store store.OrderStore
}
func newUpdateOrderHandler(store store.OrderStore) *updateOrderHandler {
return &updateOrderHandler{store: store}
}
func (h *updateOrderHandler) Handle(
ctx context.Context,
req *connect.Request[orderv1.UpdateOrderRequest],
) (*connect.Response[orderv1.UpdateOrderResponse], error) {
if err := h.validate(req.Msg); err != nil {
return nil, err
}
// Business logic + store call here
return connect.NewResponse(&orderv1.UpdateOrderResponse{
// response fields
}), nil
}
func (h *updateOrderHandler) validate(req *orderv1.UpdateOrderRequest) error {
// Validate required fields
if req.Id == "" {
return connect.NewError(connect.CodeInvalidArgument, nil)
}
return nil
}
grpc_{snake_case_method}_handler.gostore store.OrderStore fieldnewXxxHandler(store store.OrderStore) *xxxHandlerHandle(ctx, req) (resp, error)validate(req) errorEdit internal/domain/orders/server.go:
Server struct:type Server struct {
// ... existing handlers ...
updateOrderHandler *updateOrderHandler
}
NewServer():func NewServer(store store.OrderStore) *Server {
return &Server{
// ... existing handlers ...
updateOrderHandler: newUpdateOrderHandler(store),
}
}
func (s *Server) UpdateOrder(
ctx context.Context,
req *connect.Request[orderv1.UpdateOrderRequest],
) (*connect.Response[orderv1.UpdateOrderResponse], error) {
return s.updateOrderHandler.Handle(ctx, req)
}
The delegating method signature must match the Connect RPC service interface exactly.
If the handler requires a new store operation:
internal/store/order.go)type OrderStore interface {
Create(ctx context.Context, order *entity.Order) error
Get(ctx context.Context, id string) (*entity.Order, error)
List(ctx context.Context) ([]*entity.Order, error)
// Add new method:
Update(ctx context.Context, order *entity.Order) error
Close() error
}
PostgresStorefunc (s *PostgresStore) Update(ctx context.Context, order *entity.Order) error {
const query = `UPDATE orders SET item = :item, amount = :amount, status = :status WHERE id = :id`
result, err := s.db.NamedExecContext(ctx, query, order)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return ErrOrderNotFound
}
return nil
}
MockOrderStore (internal/store/mock_order_store.go)type MockOrderStore struct {
// ... existing fields ...
UpdateFunc func(ctx context.Context, order *entity.Order) error
}
func (m *MockOrderStore) Update(ctx context.Context, order *entity.Order) error {
if m.UpdateFunc != nil {
return m.UpdateFunc(ctx, order)
}
return nil
}
Existing converter in internal/domain/orders/converter.go:
entityToProto(e *entity.Order) *orderv1.Order — already exists, reuse itIf you need a new conversion direction (e.g., proto → entity), add it to the same file:
func protoToEntity(p *orderv1.Order) *entity.Order {
// conversion logic
}
Use these Connect RPC error codes consistently:
| Scenario | Code | Example |
|---|---|---|
| Invalid/missing input fields | connect.CodeInvalidArgument | connect.NewError(connect.CodeInvalidArgument, nil) |
| Entity not found | connect.CodeNotFound | connect.NewError(connect.CodeNotFound, err) |
| Authorization/ownership failure | connect.CodePermissionDenied | connect.NewError(connect.CodePermissionDenied, errors.New("...")) |
| Unexpected/internal errors | connect.CodeInternal | connect.NewError(connect.CodeInternal, err) |
For NotFound, always check with errors.Is:
if errors.Is(err, store.ErrOrderNotFound) {
return nil, connect.NewError(connect.CodeNotFound, err)
}
return nil, connect.NewError(connect.CodeInternal, err)
After creating the handler:
task build to ensure compilationtask lint to check for lint errorstask test to verify existing tests still passStudy these files to understand existing patterns:
| File | Purpose |
|---|---|
internal/domain/orders/grpc_create_order_handler.go | Create with validation, UUID generation, store.Create |
internal/domain/orders/grpc_get_order_handler.go | Read with NotFound error handling |
internal/domain/orders/grpc_check_order_owner_handler.go | Business logic + PermissionDenied |
internal/domain/orders/grpc_list_orders_handler.go | List with slice conversion |
internal/domain/orders/server.go | Handler registration and delegation |
internal/domain/orders/converter.go | Entity ↔ Proto conversion |
internal/store/order.go | OrderStore interface + PostgresStore |
internal/store/mock_order_store.go | Manual mock for testing |
internal/entity/order.go | Entity with db: tags |
Suggest to the user that they write tests using the order-test-coordinator agent, which will create both unit and isolation tests in parallel.