1
Fork 0
A minimalist blog engine API in Go: stdlib net/http routing, Postgres via pgx/v5, bcrypt passwords and HS256 JWTs. Raw SQL, no ORM, no framework — four source files you can read in one sitting. https://devai.io/templates/blog-go-postgres
  • Go 98.7%
  • Dockerfile 1.3%
Find a file Use this template
Leonardo Devai 199d778cb8 Harmonize the blog engine contract across all eight backends
published_at is now the first-publish time (null for drafts, kept across edits
and unpublish/republish), slugs collide to -2/-3 and regenerate on retitle,
every error is {"error"} with 400/401/403/404/405/409 pinned, timestamps are
RFC 3339 UTC. GUIDELINES pins the contract; each backend passes the same 59-check
end-to-end script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01128fhuZbgivaSJvtMf4s1G
2026-09-27 21:25:37 +02:00
.github/workflows Review and modernize all 42 projects to the updated standard 2026-09-27 21:10:38 +02:00
extras Review and modernize all 42 projects to the updated standard 2026-09-27 21:10:38 +02:00
.dockerignore Review and modernize all 42 projects to the updated standard 2026-09-27 21:10:38 +02:00
.env.example Review and modernize all 42 projects to the updated standard 2026-09-27 21:10:38 +02:00
.gitignore Standardize projects: compose.yaml, ./data state, CI workflow, GUIDELINES 2026-09-27 20:07:30 +02:00
auth.go Add the devai.io boilerplate library: 42 runnable projects 2026-07-19 16:31:48 +02:00
compose.yaml Review and modernize all 42 projects to the updated standard 2026-09-27 21:10:38 +02:00
db.go Add the devai.io boilerplate library: 42 runnable projects 2026-07-19 16:31:48 +02:00
Dockerfile Review and modernize all 42 projects to the updated standard 2026-09-27 21:10:38 +02:00
go.mod Review and modernize all 42 projects to the updated standard 2026-09-27 21:10:38 +02:00
go.sum Review and modernize all 42 projects to the updated standard 2026-09-27 21:10:38 +02:00
LICENSE Review and modernize all 42 projects to the updated standard 2026-09-27 21:10:38 +02:00
main.go Harmonize the blog engine contract across all eight backends 2026-09-27 21:25:37 +02:00
posts.go Harmonize the blog engine contract across all eight backends 2026-09-27 21:25:37 +02:00
README.md Harmonize the blog engine contract across all eight backends 2026-09-27 21:25:37 +02:00
schema.sql Harmonize the blog engine contract across all eight backends 2026-09-27 21:25:37 +02:00

blog-go-postgres

A minimalist blog engine API in Go: stdlib net/http routing, Postgres via pgx/v5, bcrypt passwords and HS256 JWTs. Raw SQL, no ORM, no framework — four source files you can read in one sitting.

Run

git clone https://git.devai.io/templates/blog-go-postgres.git
cd blog-go-postgres
docker compose up --build

The API answers on http://localhost:8080 (curl localhost:8080/health → ok). Postgres keeps its state in ./data/postgres; schema.sql is applied on every start (all statements are idempotent), so there is no migrate step.

Without Docker: point DATABASE_URL at any Postgres, set the variables from .env.example, then go run . (Go 1.26).

How it works

Method Path Auth Result
GET /health — 200 ok
POST /auth/register — {email, password} → 201 {id, email}, 409 if taken
POST /auth/login — {email, password} → 200 {token}, 401 if wrong
GET /posts — 200 [{id, title, slug, excerpt, published_at}], published only
GET /posts/{slug} — 200 full published post, or 404
POST /posts JWT {title, body} → 201 full post (a draft)
PUT /posts/{id} JWT {title?, body?, published?} → 200 full post
DELETE /posts/{id} JWT 204
  • Auth — register stores a bcrypt hash; login returns an HS256 JWT signed with AUTH_SECRET (sub = user id, 7-day expiry). Send it as Authorization: Bearer <token>. requireAuth in auth.go pins the algorithm, requires exp, and hands the user id to handlers via userID(r).
  • Ownership — only a post's author may update or delete it; anyone else gets 403.
  • Slugs come from the title ("Hello, World!" → hello-world); a duplicate title gets -2, -3, … Retitling a post regenerates its slug.
  • Drafts — new posts are unpublished; PUT {"published": true} publishes. Public endpoints only return published posts. excerpt is the first 200 characters of the body; published_at is stamped the first time a post is published (null until then) and kept through later edits and unpublish/republish; the list is newest first by it.
  • Errors are {"error": "message"} with a matching status code.

A full round trip:

curl -s localhost:8080/auth/register -d '{"email":"me@example.com","password":"sup3rsecret"}'
TOKEN=$(curl -s localhost:8080/auth/login -d '{"email":"me@example.com","password":"sup3rsecret"}' | jq -r .token)
ID=$(curl -s localhost:8080/posts -H "Authorization: Bearer $TOKEN" -d '{"title":"Hello, World!","body":"First post."}' | jq -r .id)
curl -s -X PUT localhost:8080/posts/$ID -H "Authorization: Bearer $TOKEN" -d '{"published":true}'
curl -s localhost:8080/posts/hello-world

Layout

main.go        server, routes, JSON helpers
db.go          pgx pool, schema apply on startup, unique-violation check
auth.go        register/login, bcrypt, JWT issue + requireAuth middleware
posts.go       post handlers, slugs, excerpts
schema.sql     users + posts tables
extras/        drop-in Clerk and Auth0 verifiers, each with swap steps

Local auth lives entirely in auth.go, so it can be replaced wholesale: extras/auth-clerk/ and extras/auth-auth0/ each hold one JWKS-based RS256 verifier and a README with the exact steps.

Deploy

Push to your own GitHub repo and the shipped workflow (.github/workflows/ci.yml) tests the compose stack, publishes the image to GHCR, and — once you set the DEPLOY_HOST / DEPLOY_USER variables and DEPLOY_KEY secret — deploys it to your server over ssh.


Part of devai.io — the blog engine series: one API contract, eight backends (blog-{go,rust,zig,python}-{postgres,mongo}) and the blog-react, blog-angular, blog-dart and blog-flutter frontends.