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:
The generated microservice ships with the full standard surface:
- CRUD —
Create,Store,Delete,Load,Lookup,List(plusMust*variants that error instead of returning a not-found boolean). - Bulk —
BulkCreate,BulkStore,BulkRevise,BulkLoad,BulkDelete. - Query —
Count,Purge. - Revision control —
Revise/MustRevise/BulkRevisefor optimistic concurrency. - REST API — standard REST routes at
/personsand/persons/{key}. - Web UI — a browser-driven form at
/web-uithat supportsGET/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
sequellibrary when aSQLDataSourceNameis 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 /personsto list.GET /persons?q.Email=harry.potter@hogwarts.edu.wizto 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 mariadbCreate 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
- SQL substrate — the
sequellibrary that backs the generated handlers. - Sequel skill (
sql-crud) — how the prompts above produce this microservice.