Skill: create-go-service
Generate the service (use-case) layer for one entity in a Go service, wired to its port interface. Use this skill whenever someone wants to add a service file to an existing Go project, implement business logic for an entity, or wire a service to an already-created repository. Trigger even if the user says "create the service for X" or "add business logic for Y entity".
Prerequisites
| Item | Action if missing |
|---|---|
go.mod in working directory |
Tell user this skill works inside an existing Go project |
internal/port/{{.EntityLower}}.go with {{.Entity}}Repository interface |
Mode A generates it inline; Mode B requires it to exist |
Step 1 — Choose mode
Ask via AskUserQuestion so the user sees a structured choice:
AskUserQuestion({
question: "How should we source the entity for this service?",
header: "Mode",
options: [
{ label: "New schema (Recommended)", description: "No repo exists yet — generate the full repository layer first via /create-go-repository, then the service" },
{ label: "Existing repo", description: "Repository layer already exists in internal/adapter/outbound/repository/ — just generate the service file" }
]
})
Mode A — New schema: User provides the entity schema. Claude generates the full repository layer first (domain + port + repository + apperr), then generates the service.
Mode B — Existing repo: Run:
find internal/adapter/outbound/repository -name "*.go" -not -name "*_test.go" 2>/dev/null
Surface the file pick using the same shape as Step 1:
- ≤4 results — wrap the pick in a second
AskUserQuestion, one option per filename. - >4 results — fall back to a numbered list with a plain text prompt.
AskUserQuestiononly carries 4 options, so once a service has more entities than that the structured UI becomes lossy.
Derive entity name from the chosen filename (e.g. order.go → Order).
Before generating, verify internal/port/{{.EntityLower}}.go contains type {{.Entity}}Repository interface. If missing, tell the user to run /create-go-repository first and stop.
Step 2 — Collect inputs
Read go.mod for {{.Module}}. Derive {{.Entity}} and {{.EntityLower}} from the picked repository file or schema.
Mode A only — parse schema: Read and follow the schema parsing rules in ${CLAUDE_SKILL_DIR}/../create-go-repository/SKILL.md (Step 4 — Collect inputs, "Schema parsing (Mode A)" subsection) to build {{.Fields}}.
Step 3 — Mode A: Generate repository layer first
Read ${CLAUDE_SKILL_DIR}/../create-go-repository/SKILL.md and execute Mode A (new schema) in full:
internal/domain/{{.EntityLower}}.gointernal/port/{{.EntityLower}}.gointernal/adapter/outbound/repository/{{.EntityLower}}.gointernal/apperr/{{.EntityLower}}.go
Run mcp__go-lsp__go_diagnose on each file and run go build ./.... Fix all errors before continuing to the service step.
Step 4 — Generate service-port interface
Template: ${CLAUDE_SKILL_DIR}/../create-go-app/references/port_service.go.tmpl
Output: internal/port/{{.EntityLower}}_service.go
This file defines port.{{.Entity}}Service — the use-case contract the HTTP
handler depends on. Both the real service and the stub implement it.
If the file already exists, ask the user before overwriting.
Step 5 — Generate service file
Template: ${CLAUDE_SKILL_DIR}/../create-go-app/references/service.go.tmpl
Output: internal/service/{{.EntityLower}}.go
Substitute {{.Entity}}, {{.EntityLower}}, {{.Module}}, {{.Fields}}.
If the file already exists, ask the user before overwriting.
Key patterns enforced by the template
- Service struct holds
repo port.{Entity}Repository— depends on the port interface, never on the concrete adapter (DIP) - Constructor:
New(repo port.{Entity}Repository) *{Entity} - Logger:
l := logger.FromContext(ctx)inside each method — never injected as struct field - Error wrapping:
fmt.Errorf("service: find {entityLower} %s: %w", id, err)— adds context, doesn't swallow - Methods:
Find,Create,Update,Delete,List,ListOffset,ListIDs— each delegates directly tos.repo.{Verb}{Entity} ListIDsis generated as a thin delegation toport.{Entity}Reader.List{Entity}IDs— exposes lightweight ID-only reads for batch and event-driven use cases- Compile-time assertion:
var _ port.{Entity}Service = (*{Entity})(nil)— fails the build if a method drifts off the port
Step 6 — Generate stub service file
Template: ${CLAUDE_SKILL_DIR}/../create-go-app/references/service_stub.go.tmpl
Output: internal/service/stub/{{.EntityLower}}.go
The stub returns canned, shape-correct responses so the frontend can integrate
against the huma OpenAPI contract while the real service is still in progress.
The file starts with //go:build stub so production builds (no -tags=stub)
exclude it.
If the file already exists, ask the user before overwriting.
Step 7 — Generate (or verify) factory files
Per-project, not per-entity. If internal/service/factory_default.go and
internal/service/factory_stub.go already exist, add the new entity's
factory function to each (alongside the existing entities' factories). If
they're missing, render both templates fresh:
| Template | Output |
|---|---|
${CLAUDE_SKILL_DIR}/../create-go-app/references/service_factory_default.go.tmpl |
internal/service/factory_default.go |
${CLAUDE_SKILL_DIR}/../create-go-app/references/service_factory_stub.go.tmpl |
internal/service/factory_stub.go |
The factories accept (cfg config.Config, repo port.{Entity}Repository) and
return port.{Entity}Service. Under the default build the factory returns
the real service unconditionally; under -tags=stub it switches on
cfg.ServiceBackend. See create-go-app/SKILL.md "Wiring Rules" for the rules
enforced.
Step 8 — Verify
mcp__go-lsp__go_diagnoseoninternal/service/{{.EntityLower}}.go,internal/service/stub/{{.EntityLower}}.go, both factory files, andinternal/port/{{.EntityLower}}_service.go. Fix all errors.go build ./...from project root. Fix all errors.go build -tags=stub ./...— must also pass clean.- Do not report done until all three pass clean.