Skip to content
Back to skills

Go Bootstrap

ASecurity

Wire a new clean-arch feature into the app — DI and route registration under internal/bootstrap/, internal/bootstrap/registry/, and cmd/api/main.go. Use when adding a feature's Setup() wiring, adding a repository/gateway accessor to the registry, or deciding whether new routes need Firebase auth, an internal API key, or no auth. Enforces the conventions below.

  • 2 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 9, 2026
devopsgobashapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 9, 2026

npx -y skills add silvioubaldino/personal-finance --skill go-bootstrap --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Go Bootstrap?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Go Bootstrap
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/silvioubaldino-go-bootstrap/badge)](https://www.skillsdirectory.com/skills/silvioubaldino-go-bootstrap)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: go-bootstrap
description: Wire a new clean-arch feature into the app — DI and route registration under internal/bootstrap/, internal/bootstrap/registry/, and cmd/api/main.go. Use when adding a feature's Setup() wiring, adding a repository/gateway accessor to the registry, or deciding whether new routes need Firebase auth, an internal API key, or no auth. Enforces the conventions below.
---

# Go Bootstrap & Dependency Injection

Authoritative description of the wiring layer that ties handler + usecase + repository
together for a feature, and the three places new wiring can land. Read this before adding a
new clean-arch feature end-to-end, or before touching route registration order in
`cmd/api/main.go`.

## When to use

- Adding `internal/bootstrap/{feature}/setup.go` for a new feature.
- Adding a new repository accessor to `internal/bootstrap/registry/registry.go`.
- Deciding which of the three `bootstrap.Setup*` entrypoints a new route belongs to.
- Touching `cmd/api/main.go` (logger/gin/middleware setup, legacy wiring).

## The pieces and how they fit

```
cmd/api/main.go
  run()
    configureLogger()                -> log.Logger
    setupGin(logger, db)              -> *gin.Engine, authentication.Authenticator
      health.Register                  (unauthenticated: /healthz, /readyz)
      r.GET("/ping")                    (unauthenticated)
      bootstrap.SetupInternalJobs        (own auth: x-api-key, under /jobs)
      bootstrap.SetupPublicComponents     (unauthenticated: webhooks, etc.)
      r.Use(authenticator.Authenticate())  <-- everything registered AFTER this needs a Firebase user_token
      r.Use(authentication.LazyProvisionUser(...))
    <legacy v1 wiring, inline>          (registered after auth -> protected)
    bootstrap.SetupCleanArchComponents(r, db, authenticator)
      -> {feature}.Setup(r, reg) per clean-arch feature:
           reg.Get{X}Repository()  ->  usecase.New{Entity}(...)  ->  api.New{Entity}V2Handlers(r, &svc)
```

## Non-negotiable rules

**`cmd/api/main.go`**
- Keep `run()` as orchestration only: load env, configure logger, init metrics, open the
  DB, call `setupGin`, wire legacy v1 (the existing inline calls — don't add to this, it's
  migration debt), call `bootstrap.SetupCleanArchComponents`, start the server.
- Pull any standalone concern into its own top-level function (`configureLogger`,
  `setupGin`, `ping`) instead of growing `run()`. `setupGin` itself owns: engine creation,
  recovery/logging/metrics/CORS middleware, health checks, `/ping`, internal jobs, public
  components, then the auth middleware — in that order.
- **Route registration order inside `setupGin` decides auth.** Anything registered before
  `r.Use(authenticator.Authenticate())` is unauthenticated by default (health, `/ping`,
  jobs, public webhooks); anything after is Firebase-protected. Never reorder these calls
  without checking which routes you're exposing or locking down.

**The three `bootstrap.Setup*` entrypoints (`internal/bootstrap/setup.go`)**
- `SetupInternalJobs(r, db)` — routes under `/jobs`, protected by
  `authentication.InternalAPIKeyAuth()` (the `x-api-key` header), not Firebase. Add a new
  job by calling `{feature}.SetupJobs(jobsGroup, reg)` here; the group's middleware already
  covers it.
- `SetupPublicComponents(r, db, auth)` — genuinely unauthenticated routes (payment-provider
  webhooks, etc.), called *before* the global Firebase middleware exists. If one of its
  routes still needs auth (e.g. a subscription endpoint mixed into an otherwise-public
  group), apply it explicitly in that feature's own `Setup()` — e.g.
  `api.NewSubscriptionHandlers(r, uc, authenticator.Authenticate())` — don't rely on global
  middleware here, it isn't attached yet.
- `SetupCleanArchComponents(r, db, auth)` — every authenticated clean-arch feature. Add your
  new feature's `{feature}.Setup(r, reg)` call here.
- Each of the three entrypoints creates its **own** `registry.NewRegistry(db)` — they don't
  share one instance, so a repository built for jobs and the same repository built for
  clean-arch components are separate Go values (harmless: both just wrap the same
  `*gorm.DB`).
- The import list in `setup.go` is alphabetical (gofumpt/goimports-enforced); the call
  order inside each `Setup*` function body has no enforced order — append new calls at the
  end.

**`internal/bootstrap/registry/registry.go`**
- One unexported pointer field + one `Get{X}Repository()` method per repository, always
  following the lazy nil-check-then-construct pattern:
  ```go
  func (r *Registry) Get{X}Repository() *repository.{X}Repository {
      if r.{x}Repository == nil {
          r.{x}Repository = repository.New{X}Repository(r.db)
      }
      return r.{x}Repository
  }
  ```
- `Registry` is a **process-lifetime singleton**, built once per `Setup*` entrypoint call —
  not per-request. Never stash per-request state on it; request-scoped data (like
  `user_id`) flows through `ctx`, not the registry.
- When a dependency composes other repositories (see `GetPlanLimitsValidator`, which needs
  the wallet/credit-card/movement/recurrent repos), build it by calling the other `Get*`
  methods — never reach into another field directly — so those dependencies stay lazily
  initialized too.
- External gateways/HTTP clients (Stripe, MercadoPago, Firebase, Expo push) are **not**
  registry-backed — they're stateless clients constructed directly inside the feature's
  `Setup()` (`gateway.NewStripeGateway()`, `push.NewExpoClient()`), never memoized on the
  registry.

**`internal/bootstrap/{feature}/setup.go`**
- `package {feature}`, one exported `Setup(r *gin.Engine, registry *registry.Registry)` (or
  `SetupJobs(jobsGroup *gin.RouterGroup, registry *registry.Registry)` for a job-only
  feature). The parameter is named `registry`, shadowing the imported package — that's
  intentional; only the type is needed in the signature, and methods are called on the
  parameter throughout the body.
- Body shape: pull every dependency from the registry (or construct a gateway/client
  inline), build the usecase(s) — innermost dependency first when one usecase depends on
  another (see `invoiceService` built before `movementService` in
  `internal/bootstrap/movement/setup.go`) — then call the handler constructor.
- If the usecase's methods use pointer receivers, take the address when passing it on:
  `movementService := usecase.NewMovement(...); api.NewMovementV2Handlers(r,
  &movementService)`. Match whatever `usecase.New{Entity}` actually returns (value vs
  pointer use — see the `go-usecases` skill).
- Wire the new `{feature}.Setup` call into `internal/bootstrap/setup.go` — either
  `SetupCleanArchComponents` (normal case) or `SetupInternalJobs` (job-only feature, calling
  `SetupJobs` instead).

## Adding a brand-new feature, end to end

1. `internal/infrastructure/repository/{feature}_repository.go` + model + `FromDomain`/`ToDomain`.
2. `internal/usecase/{feature}_usecase.go` — see the `go-usecases` skill.
3. `internal/infrastructure/api/{feature}_api.go` — see the `go-api-handlers` skill.
4. Add a `{feature}Repository *repository.{Feature}Repository` field + `Get{Feature}Repository()` to `registry.go`.
5. `internal/bootstrap/{feature}/setup.go` with `Setup(r, reg)`, following the shape above.
6. Add the import + `{feature}.Setup(r, reg)` call to `SetupCleanArchComponents` in `internal/bootstrap/setup.go`.

## Anti-patterns (reject these)

| Don't | Do |
|---|---|
| Add a new feature's wiring inline in `cmd/api/main.go`'s `run()` | Create `internal/bootstrap/{feature}/setup.go` and call it from `SetupCleanArchComponents` |
| Register a new authenticated route before `r.Use(authenticator.Authenticate())` inside `setupGin` | Register it via `SetupCleanArchComponents`, which runs after the auth middleware is attached |
| Add a repository field to `Registry` without the matching nil-check lazy getter | Always pair the field with a `Get{X}Repository()` that checks-then-constructs |
| Reach into another field directly inside a composite `Get*` (e.g. `r.walletRepository` instead of `r.GetWalletRepository()`) | Call the other `Get*` method so that dependency also gets lazily initialized |
| Memoize an external gateway/HTTP client on the `Registry` | Construct it directly in the feature's `Setup()` — gateways aren't registry-backed |
| Store request-scoped data (user ID, request context) on the `Registry` | Keep the registry process-lifetime; pass request data through `ctx` |

## Run & verify

```bash
go build ./...
make linter
go run ./cmd/api/main.go   # confirm the app boots and the new route responds as expected
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.