Enhancing Your Go REST API: Adding Database, Auth, and Testing (Part 3)
Welcome to Part 3 of our “Mastering Go Programming” series! In this tutorial, we’ll transform our basic task manager API into a production-ready service by adding essential features like database integration, authentication, and comprehensive testing.
What We’ll Cover
In this comprehensive guide, you’ll learn how to:
- Set up PostgreSQL with Go
- Implement JWT authentication
- Add request validation middleware
- Write unit and integration tests
- Document your API with Swagger
- Deploy your application securely
Prerequisites
- Complete Part 2: Build Your First REST API with Go
- PostgreSQL installed locally
- Basic understanding of SQL and authentication concepts
- Go 1.21 or later
1. Setting Up PostgreSQL Integration
First, let’s add our database dependencies:
go get -u github.com/lib/pq
go get -u github.com/golang-migrate/migrate/v4
Creating Database Migrations
Create a migrations
directory and add your first migration:
-- migrations/000001_create_tasks_table.up.sql
CREATE TABLE tasks (
id UUID PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
status VARCHAR(50) NOT NULL,
due_date TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
user_id UUID NOT NULL
);
CREATE INDEX idx_tasks_user_id ON tasks(user_id);
-- migrations/000001_create_tasks_table.down.sql
DROP TABLE IF EXISTS tasks;
Database Connection
Create internal/database/db.go
:
package database
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
type Config struct {
Host string
Port int
User string
Password string
DBName string
SSLMode string
}
func NewConnection(config *Config) (*sql.DB, error) {
connStr := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
config.Host, config.Port, config.User,
config.Password, config.DBName, config.SSLMode,
)
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, err
}
err = db.Ping()
if err != nil {
return nil, err
}
return db, nil
}
Repository Pattern Implementation
Create internal/repository/task_repository.go
:
package repository
import (
"context"
"database/sql"
"time"
"github.com/yourusername/task-manager/internal/models"
)
type TaskRepository struct {
db *sql.DB
}
func NewTaskRepository(db *sql.DB) *TaskRepository {
return &TaskRepository{db: db}
}
func (r *TaskRepository) CreateTask(ctx context.Context, task *models.Task) error {
query := `
INSERT INTO tasks (id, title, description, status, due_date, created_at, updated_at, user_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`
_, err := r.db.ExecContext(
ctx,
query,
task.ID,
task.Title,
task.Description,
task.Status,
task.DueDate,
task.CreatedAt,
task.UpdatedAt,
task.UserID,
)
return err
}
func (r *TaskRepository) GetTasksByUserID(ctx context.Context, userID string) ([]models.Task, error) {
query := `
SELECT id, title, description, status, due_date, created_at, updated_at, user_id
FROM tasks
WHERE user_id = $1
ORDER BY created_at DESC
`
rows, err := r.db.QueryContext(ctx, query, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var tasks []models.Task
for rows.Next() {
var task models.Task
err := rows.Scan(
&task.ID,
&task.Title,
&task.Description,
&task.Status,
&task.DueDate,
&task.CreatedAt,
&task.UpdatedAt,
&task.UserID,
)
if err != nil {
return nil, err
}
tasks = append(tasks, task)
}
return tasks, nil
}
2. Implementing JWT Authentication
Add the JWT dependency:
go get -u github.com/golang-jwt/jwt/v5
JWT Authentication Service
Create internal/auth/jwt.go
:
package auth
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
type JWTService struct {
secretKey []byte
expires time.Duration
}
func NewJWTService(secretKey string, expires time.Duration) *JWTService {
return &JWTService{
secretKey: []byte(secretKey),
expires: expires,
}
}
func (s *JWTService) GenerateToken(userID string) (string, error) {
claims := jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(s.expires).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.secretKey)
}
func (s *JWTService) ValidateToken(tokenString string) (string, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return s.secretKey, nil
})
if err != nil {
return "", err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
userID := claims["user_id"].(string)
return userID, nil
}
return "", errors.New("invalid token")
}
Authentication Middleware
Create pkg/middleware/auth.go
:
package middleware
import (
"context"
"net/http"
"strings"
"github.com/yourusername/task-manager/internal/auth"
)
func AuthMiddleware(jwtService *auth.JWTService) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
bearerToken := strings.Split(authHeader, " ")
if len(bearerToken) != 2 || bearerToken[0] != "Bearer" {
http.Error(w, "Invalid authorization header", http.StatusUnauthorized)
return
}
userID, err := jwtService.ValidateToken(bearerToken[1])
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "user_id", userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
3. Adding Request Validation
Create internal/validator/task_validator.go
:
package validator
import (
"errors"
"time"
"github.com/yourusername/task-manager/internal/models"
)
type TaskValidator struct{}
func (v *TaskValidator) ValidateCreate(task *models.Task) error {
if task.Title == "" {
return errors.New("title is required")
}
if len(task.Title) < 3 {
return errors.New("title must be at least 3 characters")
}
if task.DueDate.Before(time.Now()) {
return errors.New("due date must be in the future")
}
return nil
}
4. Writing Tests
Unit Tests
Create internal/handlers/task_handlers_test.go
:
package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/yourusername/task-manager/internal/models"
"github.com/stretchr/testify/assert"
)
func TestCreateTaskHandler(t *testing.T) {
tests := []struct {
name string
payload map[string]interface{}
expectedStatus int
}{
{
name: "valid task",
payload: map[string]interface{}{
"title": "Test Task",
"description": "Test Description",
"due_date": time.Now().Add(24 * time.Hour),
},
expectedStatus: http.StatusCreated,
},
{
name: "invalid task - missing title",
payload: map[string]interface{}{
"description": "Test Description",
},
expectedStatus: http.StatusBadRequest,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
payloadBytes, _ := json.Marshal(tt.payload)
req := httptest.NewRequest(
http.MethodPost,
"/api/task",
bytes.NewReader(payloadBytes),
)
rr := httptest.NewRecorder()
handler := http.HandlerFunc(CreateTaskHandler)
handler.ServeHTTP(rr, req)
assert.Equal(t, tt.expectedStatus, rr.Code)
})
}
}
Integration Tests
Create tests/integration/api_test.go
:
package integration
import (
"context"
"testing"
"net/http"
"github.com/stretchr/testify/suite"
"github.com/yourusername/task-manager/internal/database"
)
type APITestSuite struct {
suite.Suite
db *database.DB
server *http.Server
client *http.Client
}
func (s *APITestSuite) SetupSuite() {
// Initialize test database
config := &database.Config{
Host: "localhost",
Port: 5432,
User: "test",
Password: "test",
DBName: "taskmanager_test",
SSLMode: "disable",
}
db, err := database.NewConnection(config)
s.Require().NoError(err)
s.db = db
// Start test server
s.server = startTestServer()
s.client = &http.Client{}
}
func (s *APITestSuite) TearDownSuite() {
s.db.Close()
s.server.Shutdown(context.Background())
}
func (s *APITestSuite) TestCreateAndGetTask() {
// Create test JWT token
token := createTestToken(s.T())
// Test creating a task
task := createTestTask(s.T(), s.client, token)
s.Require().NotEmpty(task.ID)
// Test getting the task
tasks := getTestTasks(s.T(), s.client, token)
s.Require().Len(tasks, 1)
s.Equal(task.ID, tasks[0].ID)
}
func TestAPI(t *testing.T) {
suite.Run(t, new(APITestSuite))
}
5. API Documentation with Swagger
Install swag
:
go install github.com/swaggo/swag/cmd/swag@latest
Add Swagger annotations to your handlers:
// @title Task Manager API
// @version 1.0
// @description A task management REST API built with Go
// @host localhost:8080
// @BasePath /api
// CreateTaskHandler godoc
// @Summary Create a new task
// @Description Create a new task with the provided details
// @Tags tasks
// @Accept json
// @Produce json
// @Param task body models.Task true "Task details"
// @Success 201 {object} models.Task
// @Failure 400 {object} ErrorResponse
// @Failure 401 {object} ErrorResponse
// @Router /task [post]
func CreateTaskHandler(w http.ResponseWriter, r *http.Request) {
// ... (existing implementation)
}
Generate Swagger documentation:
swag init -g cmd/api/main.go
6. Deployment Best Practices
Docker Configuration
Create Dockerfile
:
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -o api ./cmd/api
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/api .
COPY --from=builder /app/migrations ./migrations
EXPOSE 8080
CMD ["./api"]
Create docker-compose.yml
:
version: '3.8'
services:
api:
build: .
ports:
- "8080:8080"
environment:
- DB_HOST=postgres
- DB_PORT=5432
- DB_USER=taskmanager
- DB_PASSWORD=secret
- DB_NAME=taskmanager
- JWT_SECRET=your-secret-key
depends_on:
- postgres
postgres:
image: postgres:14-alpine
environment:
- POSTGRES_USER=taskmanager
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=taskmanager
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Next Steps
Try these challenges to further enhance your API:
Add user management features:
- User registration and login
- Password reset functionality
- Role-based access control
Implement advanced task features:
- Task assignments to multiple users
- Task comments and attachments
- Task prioritization
- Recurring tasks
Add monitoring and observability:
- Prometheus metrics
- Logging with ELK stack
- Distributed tracing
Share Your Progress!
Have you implemented any of these features? Share your experience and code in the comments below. Don’t forget to star our GitHub repository for more Go programming tutorials!
Additional Resources
Last updated: January 11, 2025