Help us shape the future of Go! Take the 2026 Go Developer Survey

servicetemplate

package module
v0.0.0-...-a76b390 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: AGPL-3.0 Imports: 0 Imported by: 0

README

Service Template

The scaffold every new A-Novel backend service starts from, with an example item resource built through every layer.

X (formerly Twitter) Follow Discord


GitHub go.mod Go version GitHub repo file or directory count GitHub code size in bytes

GitHub Actions Workflow Status codecov

Coverage graph

Using this template

Fork this repository to start a service. The rest of this README is the standard service README, written for the placeholder item resource. Your fork keeps its shape. The taxonomy explains the layers every service shares.

1. Rename the service. Set your service's name on the first line, then replace every service-template reference.

SERVICE="service-<name>"

git grep -lz service-template | xargs -0 perl -pi -e "s/service-template/$SERVICE/g"

The replace covers the module path and import paths, image names and references, badges and links, and both package.json files. Rename by hand what it leaves, where git grep finds every use:

What Where From → to
Go package generate.go, pkg/go/ servicetemplate → service<name>
Env-var prefix internal/config/env/env.go SERVICE_TEMPLATE_ENV_PREFIX → SERVICE_<NAME>_ENV_PREFIX
Protobuf package internal/models/proto/anovel/template/v0 anovel.template.v0 → anovel.<name>.v0, with its folder and the templatev0 imports
JS client class pkg/js/rest/src/ TemplateApi → <Name>Api
Display name openapi.yaml, openapi.html, both README.md Service Template → your service's title
Local compose names builds/*.yaml template → <name> in container, network and volume names

2. Replace the item resource. Swap item and Item for your own resource in each layer, then adjust the fields.

Layer Files
Migration internal/models/migrations/20250306000000_items_table.{up,down}.sql, renamed with date '+%Y%m%d%H%M%S', and its testdata/ fixture
DAO internal/dao/pg.item*.{go,sql} and their tests
Core internal/core/item*.go, common.go and their tests
Handlers internal/handlers/rest.item*.go, grpc.item*.go and their tests
Proto internal/models/proto/anovel/template/v0/item*.proto
API openapi.yaml, pkg/go/client.go and its test, pkg/js/rest/src/{index,item}.ts, pkg/js/test/rest/src/item.test.ts
Wiring cmd/rest/main.go, cmd/grpc/main.go

3. Check the fork. Start its database, since the migration snapshots regenerate against it. Then regenerate the stubs, mocks and snapshots, and run every check.

a-novel run service infra start "$SERVICE"
eval "$(a-novel run env "$SERVICE")"

pnpm generate
pnpm format
pnpm lint
a-novel test -y

Then delete this section and describe your service in the rest of this README. In CONTRIBUTING.md, replace the introduction and the local run, which describe the template.

What it does

The service stores item records, each a name with an optional description, behind create, read, list, update and delete operations. It exists to be replaced: every layer, both APIs and both client packages work end to end, so a fork starts from running code.

The service ships two APIs:

  • A private gRPC API (cmd/grpc), for traffic between services on a private network. It authenticates no caller: network policy, ingress or a service mesh controls access.
  • A public REST API (cmd/rest), for any HTTP client.

Deploying

The service runs as published OCI images plus a PostgreSQL database. Both servers are stateless, so each scales to as many replicas as you need behind a load balancer; all state lives in Postgres.

Deploy the images with any container orchestrator. The composition below shows which images to run, how they wire together, and the environment they expect.

Image Role
service-template/grpc Private item CRUD + status API. Internal network only.
service-template/rest Public item CRUD + health API.
service-template/jobs/migrations One-shot schema migration job; runs to completion before the servers start.
service-template/database PostgreSQL with pgBackRest, or bring your own Postgres.

Pin every image to the same release tag. A production deployment runs database, then migrations to completion, then any number of grpc and/or rest replicas. Provide POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DATABASE through the orchestrator; store the password as a secret.

services:
  postgres-template:
    image: ghcr.io/a-novel/service-template/database:v0.0.0
    networks: [api]
    environment:
      POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
      POSTGRES_USER: "${POSTGRES_USER}"
      POSTGRES_DB: "${POSTGRES_DATABASE}"
      POSTGRES_HOST_AUTH_METHOD: scram-sha-256
      POSTGRES_INITDB_ARGS: --auth=scram-sha-256
    volumes:
      - template-postgres-data:/var/lib/postgresql/
    # Podman ignores the image's own check, so the dependents' wait needs this one.
    healthcheck:
      test: ["CMD", "pg_isready"]
      interval: 1s
      timeout: 5s
      retries: 10

  migrations-template:
    image: ghcr.io/a-novel/service-template/jobs/migrations:v0.0.0
    depends_on:
      postgres-template: { condition: service_healthy }
    environment:
      POSTGRES_HOST: postgres-template
      POSTGRES_PORT: "5432"
      POSTGRES_USER: "${POSTGRES_USER}"
      POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
      POSTGRES_DATABASE: "${POSTGRES_DATABASE}"
      POSTGRES_TLS_ENABLED: "false"
    networks: [api]

  service-template:
    image: ghcr.io/a-novel/service-template/grpc:v0.0.0 # or ghcr.io/a-novel/service-template/rest:v0.0.0
    ports: ["${GRPC_PORT}:8080"] # the container always listens on 8080; map ${REST_PORT} for the rest image
    depends_on:
      postgres-template: { condition: service_healthy }
      migrations-template: { condition: service_completed_successfully }
    environment:
      POSTGRES_HOST: postgres-template
      POSTGRES_PORT: "5432"
      POSTGRES_USER: "${POSTGRES_USER}"
      POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
      POSTGRES_DATABASE: "${POSTGRES_DATABASE}"
      POSTGRES_TLS_ENABLED: "false"
    networks: [api]

networks:
  api:

volumes:
  template-postgres-data:

Run both servers by adding a second service that reuses the same database and migrations with the rest image.

Database image

The database image packages PostgreSQL and pgBackRest on Wolfi. The container build assembles its package manifest with apko, from locks that pin every package, so an unchanged image rebuilds to the same digest. Building it takes Docker or Podman alone. The image keeps PostgreSQL's standard POSTGRES_* initialization variables, the uuid-ossp extension and the volume at /var/lib/postgresql. Backup scheduling and repository credentials stay with the operator.

Configuration

Every variable is read from the process environment. SERVICE_TEMPLATE_ENV_PREFIX prepends a prefix to every name, so the service can share an environment without collisions.

Name Description Images
POSTGRES_HOST PostgreSQL hostname or IP address. Selects the discrete connection fields. Required for new deployments. all
POSTGRES_PORT PostgreSQL port. Defaults to 5432. all
POSTGRES_USER PostgreSQL login role. Required when POSTGRES_HOST is set. all
POSTGRES_PASSWORD PostgreSQL login password. Required when POSTGRES_HOST is set; inject it as a secret. all
POSTGRES_DATABASE PostgreSQL database name. Required when POSTGRES_HOST is set. all
POSTGRES_TLS_ENABLED Encrypt the PostgreSQL connection. Defaults to true; disable only when another trusted boundary protects the database link. all
POSTGRES_DSN Connection-URL fallback, read only when POSTGRES_HOST is empty. all
Optional configuration (server tuning, OpenTelemetry)

gRPC tuning (images grpc, standalone-grpc):

Name Description Default
GRPC_TIMEOUT_SHUTDOWN Graceful shutdown budget. 30s

REST tuning (images rest, standalone-rest):

Name Description Default
REST_MAX_REQUEST_SIZE Maximum request body size, in bytes. 16384 (16KiB)
REST_CLIENT_IP_HEADER Header a trusted proxy sets to the visitor IP. Set it only when every request comes through that proxy. TCP peer
REST_LIVENESS_PATH Ping path, which still answers during a planned downtime and skips the rate limit. /v0/ping
REST_RATE_LIMITS_ENABLED Rate-limit each visitor on every route but the ping. Turn off only in test stacks. true
REST_RATE_LIMIT Requests each visitor sends to each route, as <requests>/<window>. 5/1s
REST_TIMEOUT_READ Read timeout. 15s
REST_TIMEOUT_READ_HEADER Header read timeout. 3s
REST_TIMEOUT_WRITE Write timeout. 30s
REST_TIMEOUT_IDLE Idle keep-alive timeout. 60s
REST_TIMEOUT_REQUEST Per-request timeout. 60s
REST_TIMEOUT_SHUTDOWN Graceful shutdown budget. 30s
REST_CORS_ALLOWED_ORIGINS CORS allowed origins. *
REST_CORS_ALLOWED_HEADERS CORS allowed headers. *
REST_CORS_ALLOWED_METHODS CORS allowed methods, comma-separated. HEAD,GET,POST,PUT,PATCH,DELETE
REST_CORS_ALLOW_CREDENTIALS CORS allow-credentials flag. false
REST_CORS_MAX_AGE CORS max-age, in seconds. 3600

Database connection pool (server images). The limits are per process. The database's max_connections has to cover every replica plus the migration job; the stock postgres default is 100.

Name Description Default
POSTGRES_MAX_OPEN_CONNS Maximum open connections to the database. 20
POSTGRES_MAX_IDLE_CONNS Maximum connections kept open while idle. 20

Planned downtime (server images):

Name Description Default
DOWNTIME_START When a planned downtime starts, in RFC 3339. From then until it is removed, even past the announced end, the servers answer 503 / UNAVAILABLE except for ping and gRPC health, and start without the database.

Logs and tracing — OpenTelemetry supports a stdout and a Google Cloud exporter (all server images):

Name Description Default
OTEL Enable OTel tracing; the variables below pick the exporter. false
GCLOUD_PROJECT_ID Google Cloud project ID. When set, switches the OTel exporter to GCP.
APP_NAME Application name attached to traces and logs. service-template

Using the client packages

Two clients ship with the service. Each snippet is the minimum viable call. Your editor's intellisense, pkg.go.dev and the API reference cover the full surface.

  • Go talks gRPC, for backend services.
  • JavaScript / TypeScript talks REST, for frontends and Node services.
Go (gRPC)
go get github.com/a-novel/service-template
package main

import (
	"context"
	"log"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	servicetemplate "github.com/a-novel/service-template/pkg/go"
)

func main() {
	ctx := context.Background()

	// In production, swap insecure.NewCredentials() for a TLS or mTLS credential. The server
	// authenticates no caller, so transport security alone protects it on the network.
	client, err := servicetemplate.NewClient(
		"service-template:8080",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	created, err := client.ItemCreate(ctx, &servicetemplate.ItemCreateRequest{
		Name:        "My Item",
		Description: "An optional description.",
	})
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("created item %s", created.GetItem().GetId())
}
JavaScript / TypeScript (REST)

The package is published to GitHub Packages, which requires a Personal Access Token with the read:packages scope even for public packages (why). Add to .npmrc (project root or $HOME):

@a-novel:registry=https://npm.pkg.github.com
@a-novel-kit:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${YOUR_PERSONAL_ACCESS_TOKEN}
pnpm add @a-novel/service-template-rest
import { TemplateApi, itemCreate, itemList } from "@a-novel/service-template-rest";

const api = new TemplateApi("http://service-template:8080");

const created = await itemCreate(api, "My Item", "An optional description.");
const items = await itemList(api, 10, 0);

Running locally

The standalone images bundle a server with the migrations, which they apply on every boot. Use them for a throwaway instance without the dev toolchain, never for a deployment with several replicas.

services:
  postgres-template:
    image: ghcr.io/a-novel/service-template/database:v0.0.0
    networks: [api]
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_USER: postgres
      POSTGRES_DB: postgres
      POSTGRES_HOST_AUTH_METHOD: scram-sha-256
      POSTGRES_INITDB_ARGS: --auth=scram-sha-256
    # Podman ignores the image's own check, so the dependents' wait needs this one.
    healthcheck:
      test: ["CMD", "pg_isready"]
      interval: 1s
      timeout: 5s
      retries: 10

  service-template:
    image: ghcr.io/a-novel/service-template/standalone-grpc:v0.0.0 # or standalone-rest
    ports: ["${GRPC_PORT}:8080"] # map ${REST_PORT} for the standalone-rest image
    depends_on:
      postgres-template: { condition: service_healthy }
    environment:
      POSTGRES_DSN: "postgres://postgres:postgres@postgres-template:5432/postgres?sslmode=disable"
    networks: [api]

networks:
  api:

Contributing

Platform setup and the day-to-day commands live in the developer guide. This repository's own concepts and local commands are in CONTRIBUTING.md.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
cmd
grpc command
Command grpc serves the private gRPC API, for other services on the internal network.
Command grpc serves the private gRPC API, for other services on the internal network.
migrations command
Command migrations applies the pending database schema migrations, then exits.
Command migrations applies the pending database schema migrations, then exits.
rest command
Command rest serves the public REST API, for any HTTP client.
Command rest serves the public REST API, for any HTTP client.
internal
config
Package config assembles the runtime configuration for the service: the typed structs the application reads, and the default presets that populate them from the environment.
Package config assembles the runtime configuration for the service: the typed structs the application reads, and the default presets that populate them from the environment.
config/configtest
Package configtest holds the configuration fixtures the service's tests share.
Package configtest holds the configuration fixtures the service's tests share.
config/env
Package env reads and parses the service's configuration from environment variables, exposing each setting as a typed, ready-to-use value resolved once at package initialization.
Package env reads and parses the service's configuration from environment variables, exposing each setting as a typed, ready-to-use value resolved once at package initialization.
core
Package core is the business logic layer of the service, sitting between the DAO layer (data access) and the handler layer (transport).
Package core is the business logic layer of the service, sitting between the DAO layer (data access) and the handler layer (transport).
dao
Package dao is the data access layer of the service.
Package dao is the data access layer of the service.
handlers
Package handlers is the transport layer of the service, sitting above the core layer.
Package handlers is the transport layer of the service, sitting above the core layer.
models/migrations
Package migrations holds the service's database schema as an ordered set of SQL migration files, embedded into the binary so the schema ships with the service.
Package migrations holds the service's database schema as an ordered set of SQL migration files, embedded into the binary so the schema ships with the service.
pkg
go
Package servicetemplate provides a gRPC client for the template service.
Package servicetemplate provides a gRPC client for the template service.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL