Example consuming app for modkit using MySQL, sqlc, and migrations.
- Target modkit line:
v0.x(see root stability policy) - Audience: evaluators validating production-like module composition
- See multi-module imports/exports in a realistic app
- Validate auth, validation, middleware, lifecycle, and error patterns
- Run end-to-end flows (migrate, seed, API, Swagger) with repeatable commands
- Modules:
AppModule,DatabaseModule,UsersModule,AuditModule(consumesUsersServiceexport). - Endpoints (under
/api/v1):GET /api/v1/health→{ "status": "ok" }POST /api/v1/auth/login→ demo JWT tokenPOST /api/v1/users→ create userGET /api/v1/users→ list usersGET /api/v1/users/{id}→ user payloadPUT /api/v1/users/{id}→ update userDELETE /api/v1/users/{id}→ delete user
- Swagger UI at
GET /docs/index.html(also available at/swagger/index.html) - MySQL via docker-compose for local runs.
- Testcontainers for integration smoke tests.
- Migrations and sqlc-generated queries.
- JSON request logging via
log/slog. - Errors use RFC 7807 Problem Details (
application/problem+json).
- Demo login endpoint:
POST /api/v1/auth/loginreturns a JWT. - Protected routes (require
Authorization: Bearer <token>):POST /api/v1/usersGET /api/v1/users/{id}PUT /api/v1/users/{id}DELETE /api/v1/users/{id}
- Public route:
GET /api/v1/users
make runThis starts MySQL in Docker, runs migrations locally, seeds data locally, and starts the app container.
Then hit:
curl http://localhost:8080/api/v1/health
# Login to get a token (demo credentials). Requires `jq` for parsing.
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"demo","password":"demo"}' | jq -r '.token')
# Public route
curl http://localhost:8080/api/v1/users
# Protected routes (require Authorization header)
curl -X POST http://localhost:8080/api/v1/users \
-H 'Authorization: Bearer '"$TOKEN"'' \
-H 'Content-Type: application/json' \
-d '{"name":"Ada","email":"ada@example.com"}'
curl -H 'Authorization: Bearer '"$TOKEN"'' http://localhost:8080/api/v1/users/1
curl -X PUT http://localhost:8080/api/v1/users/1 \
-H 'Authorization: Bearer '"$TOKEN"'' \
-H 'Content-Type: application/json' \
-d '{"name":"Ada Lovelace","email":"ada@example.com"}'
curl -X DELETE http://localhost:8080/api/v1/users/1 -H 'Authorization: Bearer '"$TOKEN"''
open http://localhost:8080/docs/index.htmlThe duplicate create request returns 409 Conflict with application/problem+json.
You can seed separately with:
make seedSwagger docs are checked in. To regenerate them, run:
make swaggerRequest validation returns RFC 7807 Problem Details with invalidParams for field-level errors.
Create with missing fields:
curl -X POST http://localhost:8080/api/v1/users \
-H 'Content-Type: application/json' \
-d '{"name":"","email":""}'Example response:
{
"type": "https://httpstatuses.com/400",
"title": "Bad Request",
"status": 400,
"detail": "validation failed",
"instance": "/api/v1/users",
"invalidParams": [
{ "name": "name", "reason": "is required" },
{ "name": "email", "reason": "is required" }
]
}Query parameter validation (pagination):
curl "http://localhost:8080/api/v1/users?page=-1&limit=0"Example response:
{
"type": "https://httpstatuses.com/400",
"title": "Bad Request",
"status": 400,
"detail": "validation failed",
"instance": "/api/v1/users",
"invalidParams": [
{ "name": "page", "reason": "must be >= 1" },
{ "name": "limit", "reason": "must be >= 1" }
]
}Cleanup hooks are registered on providers via ProviderDef.Cleanup. The database module uses this hook to close the *sql.DB pool.
On shutdown, the API server:
- Stops accepting new requests and waits for in-flight requests to finish.
- Runs cleanup hooks in LIFO order (last registered, first cleaned).
The users service includes a context cancellation example via Service.LongOperation, which exits early with context.Canceled when the request is canceled.
make testAPI routes are grouped under /api/v1 with scoped middleware. /docs and /swagger stay outside the group.
Applied middleware order for /api/v1:
- CORS (explicit allowed origins and methods)
- Rate limiting (
golang.org/x/time/rate) - Timing/metrics logging
Example configuration:
export CORS_ALLOWED_ORIGINS="http://localhost:3000"
export CORS_ALLOWED_METHODS="GET,POST,PUT,DELETE"
export CORS_ALLOWED_HEADERS="Content-Type,Authorization"
export RATE_LIMIT_PER_SECOND="5"
export RATE_LIMIT_BURST="10"mysqlonlocalhost:3306apponlocalhost:8080(runs migrate + seed before starting)
The compose services build from examples/hello-mysql/Dockerfile.
LOG_LEVEL defaults to info, but compose sets it to debug.
Environment variables:
HTTP_ADDR(default:8080)MYSQL_DSN(defaultroot:password@tcp(localhost:3306)/app?parseTime=true&multiStatements=true)JWT_SECRET(defaultdev-secret-change-me)JWT_ISSUER(defaulthello-mysql)JWT_TTL(default1h)AUTH_USERNAME(defaultdemo)AUTH_PASSWORD(defaultdemo)LOG_FORMAT(textorjson, defaulttext)LOG_LEVEL(debug,info,warn,error, defaultinfo)LOG_COLOR(auto,on,off, defaultauto)LOG_TIME(local,utc,none, defaultlocal)LOG_STYLE(pretty,plain,multiline, defaultpretty, text only)