yellowpages

The yellowpages.example microservice is a SQL CRUD service that persists Person records in a relational database. It demonstrates the framework’s first-class SQL CRUD support: the entire microservice — schema, indexes, CRUD endpoints, bulk variants, REST routes, and a browser-friendly Web UI — is generated by the sequel coding-agent skill from a short prompt.

It is the framework’s reference for what “boring CRUD” looks like when you don’t write it by hand.

Built with Skills

SQL CRUD microservices are first-class citizens of the Microbus framework. Rather than writing SQL boilerplate by hand, two prompts to the sequel skill produce the entire microservice:

Create a SQL CRUD microservice to store Persons. Use the hostname yellowpages.example and create it in exampleservices/yellowpages/. Skip tests.
Add the fields: FirstName (required, max 64 runes), LastName (required, max 64 runes), Email (required, max 256 runes, unique) and Birthday (time.Time, must be in the past).

The generated microservice ships with the full standard surface:

  • CRUDCreate, Store, Delete, Load, Lookup, List (plus Must* variants that error instead of returning a not-found boolean).
  • BulkBulkCreate, BulkStore, BulkRevise, BulkLoad, BulkDelete.
  • QueryCount, Purge.
  • Revision controlRevise / MustRevise / BulkRevise for optimistic concurrency.
  • REST API — standard REST routes at /persons and /persons/{key}.
  • Web UI — a browser-driven form at /web-ui that supports GET/POST/PUT/DELETE.

Depends On

  • A SQL database. The example uses an in-memory SQLite by default (no setup), and supports MySQL/MariaDB, PostgreSQL, and SQL Server via the sequel library when a SQLDataSourceName is configured.

Try It

With the examples app running, the simplest entry point is the Web UI:

http://localhost:8080/yellowpages.example/web-ui

POST to /persons to create:

{
    "firstName": "Harry",
    "lastName": "Potter",
    "email": "harry.potter@hogwarts.edu.wiz",
    "birthday": "1980-07-31T00:00:00Z"
}

Response — the new key for subsequent operations:

{
    "objKey": "a1b2c3"
}

Then:

  • PUT /persons/{key} to update.
  • GET /persons to list.
  • GET /persons?q.Email=harry.potter@hogwarts.edu.wiz to query by indexed field.
  • GET /persons/{key} to load one.
  • DELETE /persons/{key} to delete.

The OpenAPI document at http://localhost:8080/yellowpages.example/openapi.json describes every endpoint. Paste it into https://editor-next.swagger.io to try operations interactively.

Using MariaDB Instead of SQLite

docker pull mariadb
docker run -p 3306:3306 --name mariadb-1 -e MARIADB_ROOT_PASSWORD=root -d mariadb

Create the microbus database via the container’s mysql CLI:

CREATE DATABASE microbus;

Point the example at it from config.local.yaml at the repo root:

all:
  SQLDataSourceName: "root:root@tcp(127.0.0.1:3306)/microbus"

The connection string under the all: key applies to every microservice that asks for SQLDataSourceName. Yellowpages reads it on startup and connects.

Code Walkthrough

Source: exampleservices/yellowpages/service.go.

The hand-maintained file is ~1500 lines long, but most of it is generated CRUD boilerplate — 24 handlers (Create, Store, Delete, Load, List, Lookup, Count, Purge, their Must* variants, and Bulk* siblings) all in the same shape. Reading every one is unhelpful; what matters are the hooks the skill leaves for application logic.

The key spots to know about:

Tenant Scoping

func (svc *Service) tenantOf(ctx context.Context) int {
    // Returns the tenant ID for the current request.
    // ...
}

Every query and mutation reads svc.tenantOf(ctx) to scope rows by tenant. The unique-email index is also tenant-scoped (person_idx_email), so the same email can be registered once per tenant without collision. In a real deployment this would derive from the actor JWT or a routing header; the example returns a fixed value.

Column Mapping Hooks

Three hooks let the application customize how an in-memory Person translates to/from database rows:

func (svc *Service) mapColumnsOnInsert(ctx context.Context, obj *yellowpagesapi.Person) (columnMapping map[string]any, err error) {
    // Build the column map for INSERT — defaults, normalization, derived fields.
}

func (svc *Service) mapColumnsOnUpdate(ctx context.Context, obj *yellowpagesapi.Person) (columnMapping map[string]any, err error) {
    // Build the column map for UPDATE — typically a subset of insert.
}

func (svc *Service) mapColumnsOnSelect(ctx context.Context, obj *yellowpagesapi.Person) (columnMapping map[string]any, err error) {
    // Map the row's columns back into the object after SELECT.
}

These are the customization seams for things like trimming whitespace, hashing a password column, downcasing email for case-insensitive uniqueness, or splitting a time.Time into separate date/time columns. The CRUD handlers themselves stay generic; the data-shape concerns live here.

Query WHERE Clause Builder

func (svc *Service) prepareWhereClauses(ctx context.Context, query yellowpagesapi.Query) (conditions []string, args []any, err error) {
    // Translate the typed Query struct into SQL conditions + bind args.
}

This is what powers ?q.Email=... query-string filtering on List, Lookup, Count, and Purge. The Query struct’s fields (Email, FirstName, LastName) become SQL WHERE predicates. Always-applied predicates like tenant scoping are inserted here too, so they cannot be forgotten by the per-endpoint code.

A Representative CRUD Handler

func (svc *Service) Create(ctx context.Context, obj *yellowpagesapi.Person) (objKey yellowpagesapi.PersonKey, err error) {
    // ... validate, call mapColumnsOnInsert, INSERT, return generated key ...
}

Create, Store, Revise, Load, Lookup, Delete, List, Count, Purge, and their Must* / Bulk* siblings all follow a uniform shape: validate, call the appropriate mapColumnsOn* hook, issue the SQL, translate the result. They are intentionally repetitive because the skill generates them from a template — modifying one is rare; the right place to change shared behavior is the column-mapping hook or the WHERE builder above.

Demo Web UI

func (svc *Service) Demo(w http.ResponseWriter, r *http.Request) (err error) {
    // Render a form that issues GET/POST/PUT/DELETE against the CRUD endpoints.
}

Demo (mapped at /web-ui) exists because browsers can only issue GET from the address bar. The form lets you exercise the full REST surface without curl or Postman.

See Also