16 - Put It All Together
📋 Jump to TakeawaysYou've learned project structure, configuration, logging, routing, middleware, error handling, databases, templates, embedding, graceful shutdown, profiling, and deployment. Now let's see the complete bookmarks project — every file, wired together, ready to run.
This lesson has no new concepts. It's the finished product. Use it as a reference when something doesn't work, or clone it to start fresh.
The Project
A bookmarks app with two interfaces:
- JSON API —
GET,POST,DELETEon/api/bookmarksfor programmatic access - HTML UI — a server-rendered page at
/where you can add and delete bookmarks in the browser
Both share the same data layer. One store, two presentations.
Directory Structure
bookmarks/
├── main.go # Entry point: config, routes, graceful shutdown
├── config.go # Environment variables + flag overrides
├── model.go # Bookmark struct
├── store.go # Database layer (database/sql + lib/pq)
├── handler.go # JSON API handlers
├── handler_html.go # HTML handlers (list, create, delete)
├── middleware.go # Logger, Recovery, RequestID, CORS, Chain
├── templates/
│ ├── layout.html # Base HTML layout
│ └── list.html # Bookmarks page with form
├── static/
│ └── style.css # Minimal styles
├── go.mod
├── go.sum
├── .env # Local environment variables
├── Makefile # Build, run, test, lint commands
├── Dockerfile # Multi-stage build
├── docker-compose.yml # Postgres + API
└── .dockerignoreHow It Connects
The flow through the codebase:
main.goloads config, opens the database, creates the store, parses templates, registers all routes, wraps the mux in middleware, and starts the server with graceful shutdown.config.goreadsPORT,DATABASE_URL, andLOG_LEVELfrom environment variables with sensible defaults. Flags can override port and log level.model.godefines theBookmarkstruct — shared by the store, API handlers, and templates.store.goopens a*sql.DBconnection with pool settings, auto-creates the table on startup, and providesCreate,GetByID,List, andDeletemethods. All methods takecontext.Contextfor cancellation.handler.gohas the JSON API:writeJSON/writeErrorhelpers, and handlers for list, create, get, and delete. Each handler is a closure that captures the store.handler_html.gohas the HTML handlers: list renders the template, create readsr.FormValueand redirects (Post/Redirect/Get), delete takes the ID from the path and redirects.middleware.goprovidesRequestID,Recovery,CORS,Logger, and theChainfunction that wraps them in order.templates/uses Go'shtml/templatewith a layout + page pattern. The list page includes an add form and per-item delete buttons.
Running It
With Docker Compose (recommended)
docker compose upThis starts Postgres and the API. Open http://localhost:8080.
Without Docker
Start Postgres locally, then:
# Set environment variables
export DATABASE_URL="postgres://user:pass@localhost:5432/bookmarks?sslmode=disable"
export PORT=8080
# Build and run
make runOr use the .env file:
make run # Makefile loads .env automaticallyTest the API
# Create a bookmark
curl -X POST http://localhost:8080/api/bookmarks \
-H "Content-Type: application/json" \
-d '{"url": "https://go.dev", "title": "Go"}'
# List all bookmarks
curl http://localhost:8080/api/bookmarks
# Delete a bookmark
curl -X DELETE http://localhost:8080/api/bookmarks/1Or just open http://localhost:8080 in your browser and use the form.
What Each Lesson Contributed
| File | Lesson |
|---|---|
main.go |
Project Structure, Graceful Shutdown |
config.go |
Configuration |
middleware.go |
Middleware, Structured Logging |
handler.go |
HTTP Routing, Error Handling |
handler_html.go |
Templates |
store.go |
Database |
model.go |
Project Structure |
templates/ |
Templates |
Makefile, Dockerfile |
Deployment |
Every lesson added a piece. This is the assembled result.
Key Takeaways
- A production Go service is a handful of files, each with a clear responsibility
- The standard library handles routing, middleware, templates, database access, and graceful shutdown — no frameworks needed
- Separate your JSON API from your HTML handlers — they share the store but serve different clients
- Use
docker composeto run the full stack locally with one command - The complete source files are in the examples below — copy them to start your own project