Adding a Service
This guide walks through creating a new backend service in the GospeLib monorepo, registering it with Nx, and wiring it into the gateway.
Prerequisites
- The monorepo cloned and
pnpm installcompleted - Go 1.23+ (for Go services) or Python 3.12+ with uv (for Python services)
- Docker running locally
Choose Your Language
Pick the language based on the service's responsibilities:
| Choose Go when… | Choose Python when… |
|---|---|
| High-throughput routing or proxying | Graph database queries (FalkorDB) |
| Webhook handling (Stripe, Clerk) | LLM / AI API calls |
| HTTP/2 push connections (APNs/FCM) | Data transformation pipelines |
| Sub-millisecond latency is critical | Pydantic validation is needed |
Scaffold a Go Service
1. Create the directory structure
services/<name>/
├── cmd/server/main.go
├── internal/
│ ├── config/config.go
│ ├── handler/
│ ├── middleware/
│ ├── service/
│ ├── repository/
│ └── model/
├── api/openapi.yaml
├── go.mod
├── Dockerfile
└── project.json
2. Initialize the Go module
cd services/<name>
go mod init github.com/gospelib/main/services/<name>
3. Write the entry point
Follow the standard pattern in cmd/server/main.go:
package main
import (
"context"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
// 1. Configure zerolog (JSON structured logging)
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = zerolog.New(os.Stdout).With().Timestamp().Str("service", "<name>").Logger()
// 2. Create chi router with global middleware
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// 3. Register routes
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"status":"ok"}`))
})
r.Get("/ready", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"status":"ready"}`))
})
// 4. Create http.Server
port := os.Getenv("PORT")
if port == "" {
port = "8XXX" // Replace with the assigned port
}
srv := &http.Server{Addr: ":" + port, Handler: r}
// 5. Start in goroutine
go func() {
log.Info().Str("port", port).Msg("starting server")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal().Err(err).Msg("server failed")
}
}()
// 6. Wait for SIGINT/SIGTERM
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// 7. Graceful shutdown with 10s context
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal().Err(err).Msg("shutdown failed")
}
log.Info().Msg("server stopped")
}
4. Write the config loader
// internal/config/config.go
package config
import "os"
type Config struct {
Port string
DatabaseURL string
}
func Load() *Config {
return &Config{
Port: getEnv("PORT", "8XXX"),
DatabaseURL: getEnv("GOSPELIB_<NAME>_DB_URL", ""),
}
}
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
5. Add the Dockerfile
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server ./cmd/server
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
EXPOSE 8XXX
ENTRYPOINT ["/server"]
6. Add project.json
{
"name": "<name>",
"projectType": "application",
"sourceRoot": "services/<name>",
"targets": {
"build": {
"command": "cd services/<name> && go build ./cmd/server"
},
"test": {
"command": "cd services/<name> && go test ./... -race"
},
"lint": {
"command": "cd services/<name> && go vet ./... && golangci-lint run"
},
"dev": {
"command": "cd services/<name> && go run ./cmd/server"
}
}
}
Scaffold a Python Service
1. Create the directory structure
services/<name>/
├── src/gospelib_<name>/
│ ├── __init__.py
│ ├── main.py
│ ├── config.py
│ ├── routes/
│ ├── models/
│ ├── services/
│ ├── db/
│ └── utils/
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── unit/
│ └── integration/
├── pyproject.toml
├── Dockerfile
└── project.json
2. Create pyproject.toml
[project]
name = "gospelib-<name>"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115",
"uvicorn>=0.32",
"pydantic>=2.9",
"pydantic-settings>=2.6",
"structlog>=24.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.24",
"pytest-cov>=6.0",
"ruff>=0.8",
"mypy>=1.13",
]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.mypy]
strict = true
3. Write the app factory
# src/gospelib_<name>/main.py
from fastapi import FastAPI
from .config import Settings
from .routes import health
def create_app() -> FastAPI:
settings = Settings()
app = FastAPI(
title=f"GospeLib {settings.service_name}",
version="0.1.0",
)
app.include_router(health.router)
return app
4. Write the config
# src/gospelib_<name>/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
service_name: str = "<name>"
port: int = 8XXX
debug: bool = False
model_config = {"env_prefix": "GOSPELIB_<NAME>_"}
Wire Into the Gateway
After the service is running, add a proxy route in the gateway:
// In services/gateway/internal/router/router.go
r.Mount("/api/v1/<resource>", proxy.To(cfg.<Name>ServiceURL))
Add the service URL to the gateway's config:
// In services/gateway/internal/config/config.go
<Name>ServiceURL string // GOSPELIB_GATEWAY_<NAME>_URL
Register in Docker Compose
Add the service to infra/docker/compose.yml:
gospelib-<name>:
build: ../../services/<name>
ports:
- '<port>:<port>'
environment:
- GOSPELIB_<NAME>_DB_URL=...
depends_on:
- postgres
Verify It Works
- Start the service:
cd services/<name> && go run ./cmd/server(Go) oruv run uvicorn gospelib_<name>.main:create_app --factory --reload --port <port>(Python) - Check health:
curl http://localhost:<port>/health - Run tests:
go test ./... -race(Go) oruv run pytest(Python)
Checklist
- Directory structure follows the established pattern
-
project.jsonregistered with Nx targets (build, test, lint, dev) - Health and readiness endpoints at
/healthand/ready - Dockerfile with multi-stage build
- Gateway proxy route added
- Docker Compose entry added
- OpenAPI spec stub at
api/openapi.yaml - Commit scope added to
commitlint.config.mjs - Release Please component added to
release-please-config.json