tl;dr: First end-to-end Go backend. The JWT auth, repository interface, and Makefile automation are all production-shaped. The database connection is live but the handlers serve hardcoded data — the wiring between the repository and the handlers was never completed.
This was the project where Go’s conventions clicked: the application struct, the interface-based repository, keeping dependencies explicit rather than global. The patterns here are worth keeping. The gaps are equally instructive.
Repository layout
1sportz/
2├── cmd/api/
3│ ├── main.go # entry point, config, server startup
4│ ├── auth.go # JWT generation, refresh token cookie
5│ ├── db.go # connection pool
6│ ├── handlers.go # HTTP handlers
7│ ├── middleware.go # CORS
8│ ├── routes.go # Chi router setup
9│ └── utils.go # JSON read/write/error helpers
10├── internal/
11│ ├── models/
12│ │ └── Movie.go
13│ └── repository/
14│ ├── repository.go # DatabaseRepo interface
15│ └── dbrepo/
16│ └── postgres_dbrepo.go # Postgres implementation
17├── migrations/
18│ ├── 000001_create_movies_table.up.sql
19│ └── 000001_create_movies_table.down.sql
20├── docker/
21│ ├── docker-compose.yml # Postgres 14.1-alpine
22│ └── .env # DB credentials
23├── bin/
24│ └── setupPostgres.sh # docker-compose up + migrate
25└── Makefile
The application struct
The central pattern is a config struct holding all dependencies, wrapped in an application struct with a logger. Every handler is a method on application:
1type config struct {
2 port int
3 env string
4 db struct {
5 dsn string
6 connPool *sql.DB
7 }
8 jwt struct {
9 secret string
10 issuer string
11 audience string
12 cookiedomain string
13 }
14 domain string
15 auth Auth
16}
17
18type application struct {
19 config config
20 logger *log.Logger
21}
All handler dependencies come through the receiver. No globals. This is the pattern that makes Go backends testable — you can construct a test application with a different DatabaseRepo without touching the real database.
The repository interface pattern
The interface lives in internal/repository/repository.go:
1type DatabaseRepo interface {
2 AllMovies() ([]*models.Movie, error)
3}
The Postgres implementation lives in internal/repository/dbrepo/postgres_dbrepo.go:
1type PostgresDBRepo struct {
2 DB *sql.DB
3}
4
5func (m *PostgresDBRepo) AllMovies() ([]*models.Movie, error) {
6 var movies []*models.Movie
7 return movies, nil
8}
The implementation is a stub — it returns an empty slice. The intention is right: the interface separates the domain from the storage layer. Swapping in a test implementation or a different database requires only a different struct that satisfies the same interface.
The gap: the application struct never holds a DatabaseRepo field. The handlers do not call through the repository. The pattern exists and is correct in shape, but the wiring was never finished. The AllMovies handler returns two hardcoded movies directly.
JWT auth
Auth in auth.go handles token generation:
1type Auth struct {
2 Issuer string
3 Audience string
4 Secret string
5 TokenExpiry time.Duration // 15 minutes
6 RefreshExpiry time.Duration // 2 hours
7 CookieDomain string
8 CookiePath string
9 CoookieName string // typo: three o's
10}
GenerateTokenPair issues two tokens with HS256 signing:
1type TokenPairs struct {
2 Token string `json:"access_token"`
3 RefreshToken string `json:"refresh_token"`
4}
Access token claims: name, sub (user ID), aud, iss, iat, exp (15 min), typ: "JWT". Refresh token claims: sub, iat, exp (2 hours).
The refresh token is set as an HttpOnly cookie:
1http.Cookie{
2 Name: "__Host-refresh_token",
3 HttpOnly: true,
4 Secure: true,
5 SameSite: http.SameSiteStrictMode,
6 MaxAge: int(app.config.auth.RefreshExpiry.Seconds()),
7}
__Host- prefix enforces that the cookie can only be set on HTTPS with no Domain attribute — a meaningful security constraint, not decoration. SameSite: Strict prevents the cookie being sent on cross-site navigations.
The auth handler itself is incomplete:
1func (app *application) authenticate(w http.ResponseWriter, r *http.Request) {
2 var jwtUser = jwtUser{
3 ID: 1,
4 FirstName: "Admin",
5 LastName: "User",
6 }
7 // always returns tokens for the hardcoded user
8 tokenPairs, err := app.config.auth.GenerateTokenPair(&jwtUser)
9 ...
10}
No credential lookup, no password check, no database call. Any request to /authenticate returns a valid JWT for user ID 1. The token infrastructure is correct; the authentication step is missing.
Routing
Chi v5 with three routes:
1mux.Use(middleware.Recoverer)
2mux.Use(app.enableCORS)
3
4mux.Get("/", app.Home)
5mux.Get("/movies", app.AllMovies)
6mux.Get("/authenticate", app.authenticate)
CORS is hardcoded to http://localhost:5173 (the Vite dev server for the React frontend). AllMovies returns two hardcoded entries: Highlander (1986) and Raiders of the Lost Ark (1981).
The database schema
1CREATE TABLE IF NOT EXISTS movies (
2 id bigserial PRIMARY KEY,
3 created_at timestamp(0) with time zone NOT NULL DEFAULT NOW(),
4 title text NOT NULL,
5 year integer NOT NULL,
6 runtime integer NOT NULL,
7 genres text[] NOT NULL,
8 version integer NOT NULL DEFAULT 1
9);
The Movie model in internal/models/Movie.go:
1type Movie struct {
2 ID int `json:"id"`
3 Title string `json:"title"`
4 ReleaseDate time.Time `json:"release_date"`
5 RunTime int `json:"runtime"`
6 MPAARating string `json:"mpaa_rating"`
7 Description string `json:"description"`
8 Image string `json:"image"`
9 CreatedAt time.Time `json:"-"`
10 UpdatedAt time.Time `json:"-"`
11}
The schema and the model do not match. The schema stores year (integer) and genres (text array); the model has ReleaseDate (time.Time), MPAARating, Description, and Image — none of which exist in the schema. A query mapping rows from the movies table into Movie structs would require a scan that maps nothing correctly.
Database connection
db.go uses the pgx driver registered under the database/sql interface:
1db, err := sql.Open("pgx", dsn)
go.mod imports both github.com/jackc/pgx/v4 and github.com/lib/pq. Only pgx is used. lib/pq is unused.
Connection pool is opened, pinged, and its close deferred in main. No pool size tuning, no context on the ping — reasonable for a learning project.
The Makefile
1run/api: ## start postgres, then run the API
2 @make start_postgres
3 go run ./cmd/api --port ${PORT} --env ${ENV}
4
5build: ## build binary
6 go build ./cmd/api/
7
8vet: ## vet + fmt
9 go vet ./...
10 go fmt ./...
run/api chains start_postgres (which runs ./bin/setupPostgres.sh, which runs docker-compose and applies migrations) then runs the API. This is the right shape for local development: one command brings up the dependency and starts the server.
setupPostgres.sh exports FUTSAL_POSTGRES_DSN — a leftover variable name from a previous project. The migration command uses ${POSTGRES_DSN}, which is the correct name. Whether this was ever tested end-to-end is unclear.
JSON utilities
utils.go contains three functions used by every handler:
1func (app *application) writeJSON(w http.ResponseWriter, status int, data interface{}, headers ...http.Header) error
2
3func (app *application) readJSON(w http.ResponseWriter, r *http.Request, data interface{}) error
4
5func (app *application) errorJSON(w http.ResponseWriter, err error, status ...int) error
readJSON caps the request body at 1MB, disallows unknown fields, and rejects requests with more than one JSON value. These are the constraints you want on a public API endpoint. They are wired correctly.
errorJSON wraps errors in a consistent response envelope:
1type JSONResponse struct {
2 Error bool `json:"error"`
3 Message string `json:"message"`
4 Data interface{} `json:"data,omitempty"`
5}
What the project name says versus what the schema says
The repository is named sportz. The Docker database is named movies. The model is Movie. The migration creates a movies table. The two hardcoded responses in AllMovies are films. There is no sports data anywhere in the code. The project was either renamed mid-build or the domain was never settled on.
What works
- JWT token pair generation with proper claims and expiry
- Refresh token as a correctly-attributed HttpOnly secure cookie
- Chi router with panic recovery and CORS middleware
- Repository interface in the right package (
internal/repository) — private to the module, not exported as a package applicationstruct pattern with no globals- JSON utilities that handle malformed input
- Migration files that define a coherent schema
- Docker Compose + golang-migrate wired through a shell script and a Makefile target
- Server configured with explicit read/write/idle timeouts
What is incomplete
- Repository not injected into
application— handlers cannot reach the database through the interface authenticatehas no credential logic — issues tokens for a hardcoded user unconditionallyAllMoviesreturns hardcoded data — never queries PostgresMoviemodel fields do not match the database schema- CORS origin hardcoded to localhost — not configurable for deployment
CoookieNamehas a typo (three o’s)setupPostgres.shreferences a stale environment variable name from a previous project- lib/pq in go.mod is unused