| name | go-backend |
| description | Go backend development guidance for pfinance. Use when working on backend services, adding new endpoints, writing tests, or debugging server issues. |
Go Backend Development
This skill covers backend development patterns for the pfinance Go server.
Project Structure
backend/
├── cmd/server/
│ └── main.go # Server entrypoint
├── internal/
│ ├── auth/
│ │ ├── firebase.go # Firebase Auth validation
│ │ ├── interceptor.go # Auth middleware
│ │ └── local_dev.go # Dev mode auth bypass
│ ├── service/
│ │ ├── finance_service.go # Main service impl
│ │ ├── finance_service_test.go # Service tests
│ │ └── budget_service_test.go # Budget tests
│ └── store/
│ ├── store.go # Store interface
│ ├── firestore.go # Firestore implementation
│ ├── memory.go # In-memory implementation
│ └── store_mock.go # Generated mocks
└── gen/pfinance/v1/ # Generated protobuf code
Running the Backend
make dev-backend
cd backend
export PORT=8111
export USE_MEMORY_STORE=true
go run cmd/server/main.go
export GOOGLE_CLOUD_PROJECT=pfinance-app-1748773335
export USE_MEMORY_STORE=false
go run cmd/server/main.go
Adding a New Service Method
1. Define in Proto (see protobuf-workflow skill)
2. Implement the Handler
func (s *FinanceService) NewMethod(
ctx context.Context,
req *connect.Request[v1.NewMethodRequest],
) (*connect.Response[v1.NewMethodResponse], error) {
userID := auth.UserIDFromContext(ctx)
if userID == "" {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("user not authenticated"))
}
if req.Msg.RequiredField == "" {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("required_field is required"))
}
result, err := s.store.SomeOperation(ctx, userID, req.Msg)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
return connect.NewResponse(&v1.NewMethodResponse{
Result: result,
}), nil
}
3. Add Store Method (if needed)
type Store interface {
SomeOperation(ctx context.Context, userID string, input *SomeInput) (*SomeOutput, error)
}
func (s *MemoryStore) SomeOperation(ctx context.Context, userID string, input *SomeInput) (*SomeOutput, error) {
s.mu.Lock()
defer s.mu.Unlock()
}
func (s *FirestoreStore) SomeOperation(ctx context.Context, userID string, input *SomeInput) (*SomeOutput, error) {
}
4. Regenerate Mocks
cd backend
go generate ./internal/store
Testing
Service Tests with Mocks
func TestCreateExpense(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockStore := store.NewMockStore(ctrl)
svc := NewFinanceService(mockStore)
mockStore.EXPECT().
CreateExpense(gomock.Any(), gomock.Any()).
Return(&v1.Expense{Id: "exp-123"}, nil)
ctx := auth.ContextWithUserID(context.Background(), "user-123")
req := connect.NewRequest(&v1.CreateExpenseRequest{
UserId: "user-123",
Description: "Test expense",
Amount: 50.00,
})
resp, err := svc.CreateExpense(ctx, req)
require.NoError(t, err)
assert.Equal(t, "exp-123", resp.Msg.Expense.Id)
}
Running Tests
make test-backend
cd backend && go test ./internal/service -v
cd backend && go test ./internal/service -run TestCreateExpense -v
cd backend && go test ./... -coverprofile=coverage.out
Connect-RPC Error Handling
import "github.com/bufbuild/connect-go"
connect.CodeInvalidArgument
connect.CodeUnauthenticated
connect.CodePermissionDenied
connect.CodeNotFound
connect.CodeAlreadyExists
connect.CodeInternal
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("expense %s not found", id))
Middleware/Interceptors
func AuthInterceptor() connect.UnaryInterceptorFunc {
return func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
ctx = ContextWithUserID(ctx, userID)
return next(ctx, req)
}
}
}
Best Practices
- Always use the Store interface - enables easy mocking
- Extract user ID from context - set by auth interceptor
- Use proper Connect error codes - helps frontend handling
- Write tests with gomock -
go generate ./internal/store
- Keep handlers thin - business logic in store or separate packages