Skip to main content
The microservices-app project uses pre-commit hooks to enforce code quality standards locally before changes reach CI. Hooks are automatically installed and managed by devenv.

Automatic Setup

Pre-commit hooks are configured in devenv.nix and installed automatically when you enter the devenv shell:
cd microservice-app
direnv allow  # Activates devenv, installs hooks
No manual pre-commit install needed. Devenv manages the git hooks lifecycle.

Configured Hooks

treefmt

Purpose: Multi-language formatter for Nix, Go, and TypeScript Configuration:
treefmt = {
  enable = true;
  config.programs = import ./treefmt-programs.nix;
};
Formats:
  • Nix - nixfmt or nixpkgs-fmt (configured in treefmt-programs.nix)
  • Go - gofmt (via golangci-lint)
  • TypeScript - Biome formatter
Runs on: All modified files in the commit Manual usage:
fmt  # Format all files and stage with git add -u

golangci-lint

Purpose: Go code linting and static analysis Configuration:
golangci-lint = {
  enable = true;
  entry = "bash -c 'export PATH=\"$DEVENV_ROOT/.devenv/go-bin:$PATH\" && cd services && golangci-lint run'";
  files = "\\.go$";
  excludes = [ "^services/gen/go/" ];
  pass_filenames = false;
};
Runs on: All .go files except generated code in services/gen/go/ Runs: Full golangci-lint run in the services/ directory Checks include:
  • Dead code detection
  • Code complexity
  • Error handling patterns
  • Style violations
  • Security issues
Manual usage:
cd services
golangci-lint run ./...

goimports

Purpose: Go import statement organization and formatting Configuration:
gofmt = {
  enable = true;
  name = "goimports";
  entry = "bash -c 'export PATH=\"$DEVENV_ROOT/.devenv/go-bin:$PATH\" && cd services && goimports -l -w .'";
  files = "\\.go$";
  excludes = [ "^services/gen/go/" ];
  pass_filenames = false;
};
Runs on: All .go files except generated code Actions:
  • Adds missing imports
  • Removes unused imports
  • Organizes imports into groups (stdlib, third-party, local)
  • Formats code with gofmt
Flags:
  • -l - List files with incorrect formatting
  • -w - Write changes back to files
Manual usage:
cd services
goimports -l -w .

biome (Frontend)

Purpose: TypeScript and React linting/formatting Configuration:
biome = {
  enable = true;
  name = "biome check";
  entry = "bash -c 'cd frontend && npx biome check src/'";
  files = "\\.(ts|tsx)$";
  pass_filenames = false;
};
Runs on: All .ts and .tsx files in the frontend Checks:
  • TypeScript linting rules
  • React best practices
  • Code formatting (Prettier-compatible)
  • Import sorting
Biome advantages:
  • 20-30x faster than ESLint + Prettier
  • Single tool for linting and formatting
  • Automatic fixes for many issues
Manual usage:
cd frontend
npx biome check src/          # Check only
npx biome check --apply src/  # Apply safe fixes
npx biome format --write src/ # Format only

go test

Purpose: Run Go unit tests before committing Configuration:
go-test = {
  enable = true;
  name = "go test";
  entry = "bash -c 'cd services && go test ./...'";
  files = "\\.go$";
  excludes = [ "^services/gen/go/" ];
  pass_filenames = false;
};
Runs on: All .go files except generated code Runs: Complete test suite with go test ./... Prevents:
  • Committing code that breaks existing tests
  • Regression bugs
  • Incomplete implementations
Manual usage:
cd services
go test ./...                    # All tests
go test ./internal/greeter/...   # Package tests
go test -v -run TestSayHello     # Specific test

Hook Execution Flow

When you run git commit, hooks execute in this order:
  1. treefmt - Format all modified files
  2. goimports - Organize Go imports (runs even if treefmt fails)
  3. golangci-lint - Lint Go code
  4. biome - Lint TypeScript/React
  5. go test - Run Go unit tests
If any hook fails, the commit is blocked. Fix the issues and try again.

Hook Behavior

pass_filenames: false

All hooks use pass_filenames = false, meaning they run on the entire codebase (within their directory), not just staged files. Why: Go and TypeScript projects have complex dependencies. Checking only staged files could miss:
  • Import changes affecting other files
  • Interface changes breaking implementations
  • Test failures in dependent packages

Excluding Generated Code

Generated code directories are excluded:
excludes = [ "^services/gen/go/" ];
Directories excluded:
  • services/gen/go/ - Protobuf-generated Go code (buf generate)
  • frontend/src/gen/ - Protobuf-generated TypeScript (buf generate)
Generated code is formatted by its generators and shouldn’t be manually edited. In rare cases (emergency hotfix, hook bugs), you can skip hooks:
git commit --no-verify -m "Emergency fix"
Warning: CI will still run the same checks. Bypassing hooks means you’ll find issues later in CI, wasting time.

Hook Failure Troubleshooting

treefmt Failures

# Run treefmt manually to see formatting issues
treefmt --no-cache

# Or use the devenv script
fmt
Treefmt usually auto-fixes issues. Re-stage changes and commit again.

golangci-lint Failures

# See detailed lint errors
cd services
golangci-lint run ./...

# Some linters support auto-fix
golangci-lint run --fix ./...
Common issues:
  • Unused variables: Remove them or prefix with _
  • Unchecked errors: Add proper error handling
  • Code complexity: Refactor long functions

goimports Failures

Rare - goimports usually succeeds or auto-fixes. If it fails:
cd services
goimports -l -w .
Check for:
  • Syntax errors in Go files
  • Invalid import paths

biome Failures

cd frontend

# See all issues
npx biome check src/

# Apply safe auto-fixes
npx biome check --apply src/

# Apply ALL fixes (including unsafe)
npx biome check --apply-unsafe src/
Note: Review unsafe fixes before committing.

go test Failures

cd services

# Run tests with verbose output
go test -v ./...

# Run specific failing test
go test -v -run TestName ./internal/package/

# Run tests with race detection
go test -race ./...
Fix failing tests before committing. Don’t skip this hook.

Manual Hook Testing

Test hooks without committing:
# Test a specific hook
pre-commit run treefmt --all-files
pre-commit run golangci-lint --all-files
pre-commit run go-test --all-files

# Run all hooks
pre-commit run --all-files
Note: The pre-commit command may not be available since devenv manages hooks directly. Use the manual commands for each tool instead.

Devenv Scripts vs Hooks

Devenv provides convenience scripts that run similar checks:
ScriptEquivalent HooksUsage
fmttreefmt + goimportsFormat all code and stage changes
lintgolangci-lint + biomeLint all code (no auto-fix)
Use these scripts before committing to catch issues early:
fmt   # Format everything
lint  # Check for lint errors

Hook Performance

Typical hook execution times:
HookTime (small change)Time (large change)
treefmt1-2s3-5s
goimports1-2s2-4s
golangci-lint5-10s15-30s
biome0.5-1s1-2s
go test3-10s10-30s
Total: ~10-20s for typical commits. This is much faster than waiting for CI failures.

Relationship to CI

Pre-commit hooks run the same checks as CI:
HookCI JobPurpose
golangci-lintgo-lintGo linting
go testgo-testGo unit tests
biomefrontend-lint + node-lintTypeScript linting
Benefit: Catch issues locally in seconds instead of waiting minutes for CI feedback. CI runs additional checks:
  • buf lint and buf breaking - Proto validation
  • npm run build - TypeScript compilation and Vite build
  • nix build - Reproducible binary builds

Customizing Hooks

Hooks are configured in devenv.nix under the git-hooks.hooks section. To modify:
  1. Edit devenv.nix
  2. Exit and re-enter the devenv shell:
    exit
    direnv allow
    
  3. Hooks are automatically reinstalled
Example: Disable go test hook for faster commits during development:
go-test = {
  enable = false;  # Change to false
  # ...
};
Recommendation: Keep all hooks enabled. They prevent CI failures and maintain code quality.

Best Practices

  1. Run fmt before committing - Catches formatting issues instantly
  2. Commit frequently - Smaller commits = faster hook execution
  3. Don’t bypass hooks - They save time by catching issues early
  4. Fix hook failures immediately - Don’t accumulate issues
  5. Keep generated code out of PRs - Run buf generate locally, hooks ignore it

Build docs developers (and LLMs) love