Chirpy
A backend HTTP API for a Twitter-style microblogging service, written in Go on the standard library's net/http router. Users post short "chirps", authenticate with Argon2id-hashed passwords, and hold a short-lived JWT access token paired with a long-lived, revocable refresh token. Persistence is PostgreSQL, accessed through type-safe query code generated by sqlc rather than an ORM.
Highlights
- $ Routing built on Go 1.22's enhanced net/http ServeMux — method-aware patterns like GET /api/chirps/{chirp_id} and path wildcards — with no third-party web framework.
- $ Type-safe database layer generated by sqlc from hand-written SQL: schema migrations in sql/schema (goose format), queries in sql/queries, generated Go in internal/database.
- $ Two-token auth flow: login issues an HS256 JWT access token (1-hour expiry) plus an opaque 32-byte refresh token (crypto/rand, 60-day expiry); /api/refresh mints a new access token and /api/revoke marks the refresh token revoked in Postgres.
- $ Passwords hashed with Argon2id (alexedwards/argon2id), a memory-hard KDF, rather than bcrypt.
- $ Metrics middleware wraps the static file server and counts hits with an atomic.Int32, surfaced at /admin/metrics; the destructive /admin/reset endpoint returns 403 unless PLATFORM=dev.
- $ Chirp validation enforces a 140-character limit and runs a profanity filter that masks a blocklist of words before the row is inserted.
Architecture
Trade-offs & decisions
Standard-library net/http vs. a web framework
Go 1.22's ServeMux handles method-specific routes and path parameters natively, so the project uses it directly instead of chi, gin, or echo. Fewer dependencies and nothing sitting between the handler and the request, at the cost of writing middleware, path parsing, and JSON error helpers by hand.
sqlc code generation vs. an ORM
SQL is written by hand in sql/queries and sqlc generates type-checked Go from it against the real schema. Queries stay explicit and are verified at build time, trading away an ORM query builder and automatic migrations for a codegen step that has to be re-run whenever a query or the schema changes.
Short-lived JWT plus a stored refresh token vs. stateless JWT alone
Access tokens expire after an hour and carry no server state, while refresh tokens live in Postgres so they can be revoked on logout. This adds a database round-trip and a token table to maintain, but a leaked access token is only useful briefly and sessions can actually be ended.
Code excerpt
func MakeJWT(userID uuid.UUID, tokenSecret string, expiresIn time.Duration) (string, error) {
claims := jwt.RegisteredClaims{
Issuer: "chirpy-access",
IssuedAt: jwt.NewNumericDate(time.Now().UTC()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiresIn)),
Subject: userID.String(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(tokenSecret))
}
func ValidateJWT(tokenString, tokenSecret string) (uuid.UUID, error) {
token, err := jwt.ParseWithClaims(
tokenString,
&jwt.RegisteredClaims{},
func(token *jwt.Token) (interface{}, error) {
return []byte(tokenSecret), nil
},
)
if err != nil {
return uuid.Nil, err
}
subject, err := token.Claims.GetSubject()
if err != nil {
return uuid.Nil, err
}
return uuid.Parse(subject)
}