Skip to main content

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 install completed
  • 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 proxyingGraph database queries (FalkorDB)
Webhook handling (Stripe, Clerk)LLM / AI API calls
HTTP/2 push connections (APNs/FCM)Data transformation pipelines
Sub-millisecond latency is criticalPydantic 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

  1. Start the service: cd services/<name> && go run ./cmd/server (Go) or uv run uvicorn gospelib_<name>.main:create_app --factory --reload --port <port> (Python)
  2. Check health: curl http://localhost:<port>/health
  3. Run tests: go test ./... -race (Go) or uv run pytest (Python)

Checklist

  • Directory structure follows the established pattern
  • project.json registered with Nx targets (build, test, lint, dev)
  • Health and readiness endpoints at /health and /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