This is a full-stack, production-ready backend service for a Twitter-like application called Chirpy, built entirely in Go.
This project was built to gain hands-on experience with the full lifecycle of a modern backend application: from designing a REST API and securing it, to managing a persistent database and handling auth, all the way to containerizing the final application with Docker.
- RESTful API: A clean, idiomatic Go API built using the
chirouter for all core application logic (users, chirps, etc.). - User Authentication: Full authentication and authorization system using JSON Web Tokens (JWTs) for both access and refresh tokens.
- Password Security: User passwords are never stored in plaintext. They are securely hashed using the
bcryptalgorithm. - Protected Endpoints: A custom authentication middleware (
middlewareAuth.go) is used to protect specific API endpoints, validating the JWT bearer token on incoming requests. - Persistent Postgres Database: All application data (users, chirps) is stored in a Postgres database.
- Type-Safe Database Queries: Uses
sqlcto generate type-safe Go code directly from raw SQL queries, providing the speed of raw SQL with the safety of an ORM. - Database Migrations: The database schema is version-controlled and managed using a series of SQL migration files.
- Containerized: Includes a
Dockerfilefor building and shipping the final application as a lightweight container.
A core part of this project is the internal/auth package, which is responsible for creating and validating tokens. New access tokens are created for users upon successful login and are signed using a JWT secret.
// From: internal/auth/auth.go
// MakeJWT generates a new JWT for a user
func MakeJWT(userID int, tokenSecret string, expiresIn time.Duration) (string, error) {
signingKey := []byte(tokenSecret)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
Issuer: "chirpy",
IssuedAt: jwt.NewNumericDate(time.Now().UTC()),
ExpiresAt: jwt.NewNumericDate(time.Now().UTC().Add(expiresIn)),
Subject: fmt.Sprintf("%d", userID),
})
return token.SignedString(signingKey)
}
This token is then validated by a middleware on protected routes, which checks the signature and extracts the userID from the token's claims.- Secure User Creation with bcrypt When a new user is created, their password is processed by bcrypt before being stored in the database. This ensures that even if the database is compromised, the user passwords are not.
// From: internal/database/db.go
// CreateUser creates a new user and saves it to the database
func (db *DB) CreateUser(email, password string) (User, error) {
hashedPassword, err := HashPassword(password) // Hash the password
if err != nil {
return User{}, err
}
params := CreateUserParams{
Email: email,
HashedPassword: hashedPassword, // Store the hash, not the password
}
user, err := db.DB.CreateUser(context.Background(), params)
if err != nil {
return User{}, err
}
return user, nil
}- Type-Safe SQL with sqlc Instead of using a heavy ORM or writing raw SQL strings in Go, this project uses sqlc. I write standard SQL queries (schema and queries), and sqlc generates idiomatic, type-safe Go code to execute them. This prevents SQL injection and catches errors at compile-time.
-- name: CreateUser :one
INSERT INTO users (email, hashed_password)
VALUES ($1, $2)
RETURNING *;- Auto-Generated Go Code (internal/database/users.sql.go):
const createUser = `-- name: CreateUser :one
INSERT INTO users (email, hashed_password)
VALUES ($1, $2)
RETURNING id, email, hashed_password
`
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, createUser, arg.Email, arg.HashedPassword)
var i User
err := row.Scan(&i.Id, &i.Email, &i.HashedPassword)
return i, err
}-
Make sure you have Go and Docker installed.
-
A database.json file is required to run (or you can modify main.go to connect to a live Postgres instance).
-
Build and run the server:
go run .
The server will start on http://localhost:8080.