Skip to content

nessergio/api-demo

Repository files navigation

Huma API Demo

A comparative demonstration of building REST APIs in Go, showcasing the advantages of Huma over traditional frameworks like Gin.

This project implements the same blog post API using two different approaches:

  • API v1 (/api/v1/posts) - Traditional Gin framework
  • API v2 (/api/v2/posts) - Huma with Gin adapter

Why Huma is Better

1. Auto-Generated OpenAPI 3.1 Documentation

Huma: Documentation is automatically generated from your code structure, always in sync with implementation.

type IdInput struct {
    Id int `path:"id" minimum:"0" example:"100" doc:"ID of the post"`
}

Gin: Requires manual documentation with tools like Swaggo, prone to drift from actual implementation.

See it in action: Navigate to http://localhost:8080/doc for interactive API documentation with downloadable OpenAPI specs.

2. Type-Safe Input/Output

Huma: Strongly-typed request/response structs with automatic validation.

func (pc *PostsController) readByIdHandler(ctx context.Context, input *IdInput) (*PostSingle, error)

Gin: Generic gin.Context with manual type assertions and conversions.

func (pc *PostsController) readByIdHandler(c *gin.Context)

Compare: v2/posts_controller.go:38 vs v1/posts_controller.go:88

3. Declarative Validation

Huma: Built-in validation via struct tags, reducing boilerplate.

type CreatePostCommand struct {
    Body struct {
        Author  string `json:"author" maxLength:"50" doc:"Author of the post"`
        Title   string `json:"title" maxLength:"50" doc:"Title of the post"`
        Content string `json:"content" maxLength:"5000" doc:"Content"`
    }
}

Gin: Manual validation code scattered throughout handlers.

Compare: v2/posts_controller.go:59-66 vs v1/posts_controller.go:20-34

4. Automatic Parameter Parsing & Validation

Huma: Path/query parameters are automatically parsed, type-checked, and validated.

type IdInput struct {
    Id int `path:"id" minimum:"0" example:"100" doc:"ID of the post"`
}

Gin: Manual string-to-int conversion with error handling in every handler.

id, err := strconv.Atoi(c.Param("id"))
if err != nil {
    _ = c.Error(err)
    _ = c.AbortWithError(http.StatusUnprocessableEntity, fmt.Errorf("invalid id"))
    return
}

Compare: v2/posts_controller.go:38 vs v1/posts_controller.go:88-93

5. Semantic Error Handling

Huma: Clean, semantic error constructors with proper RFC 9457 Problem Details.

return nil, huma.Error404NotFound("post not found", err)

Gin: Manual status code management.

_ = c.AbortWithError(http.StatusNotFound, err)
return

Compare: v2/posts_controller.go:42 vs v1/posts_controller.go:96-99

6. Less Boilerplate, More Clarity

Lines of code comparison for the same functionality:

While v2 has more lines, the code is more declarative and maintainable. The extra lines are mostly struct definitions that serve as both documentation and validation schema.

7. Built-in API Documentation UI

Huma provides a beautiful, interactive documentation interface out of the box at /doc. No configuration needed.

Available Endpoints

Endpoint Description
GET /api/v1/posts List all posts (Gin)
GET /api/v1/posts/:id Get post by ID (Gin)
POST /api/v1/posts Create new post (Gin)
PUT /api/v1/posts/:id Update post (Gin)
DELETE /api/v1/posts/:id Delete post (Gin)
GET /api/v2/posts List all posts (Huma)
GET /api/v2/posts/{id} Get post by ID (Huma)
POST /api/v2/posts Create new post (Huma)
PUT /api/v2/posts/{id} Update post (Huma)
DELETE /api/v2/posts/{id} Delete post (Huma)
GET /healthcheck Health check endpoint
GET /doc Interactive API documentation (OpenAPI 3.1)

Quick Start

Fetching Dependencies

make dep

or

go mod tidy

Building & Running

make run

or

go build -o bin/api-demo cmd/main.go && ./bin/api-demo

The server will start on http://localhost:8080

Testing

make test

or

go test -v ./tests/unit/...

Project Structure

.
├── cmd/
│   └── main.go                 # Application entry point
├── config/
│   └── config.go              # Configuration management
├── models/
│   └── posts/                 # Post model and data layer
├── routes/
│   ├── api/
│   │   ├── v1/                # Gin implementation
│   │   └── v2/                # Huma implementation
│   ├── middleware/            # Custom middleware
│   └── router.go              # Main router setup
└── tests/
    └── unit/                  # Unit tests for both versions

Try It Yourself

  1. Start the server: make run
  2. Open http://localhost:8080/doc
  3. Compare the implementations:
    • Try creating a post with invalid data to see validation in action
    • Notice how v2 provides detailed error messages with proper structure
    • Observe the auto-generated documentation showing validation rules
    • Download the OpenAPI schema and use it with code generators

API Usage Examples

The server comes pre-loaded with sample blog posts from cmd/blog_data.json. Below are examples of how to interact with both API versions.

Health Check

curl http://localhost:8080/healthcheck
# Response: pong

List All Posts

v1 (Gin):

curl http://localhost:8080/api/v1/posts

v2 (Huma):

curl http://localhost:8080/api/v2/posts

Response Example:

{
  "result": [
    {
      "id": 1,
      "title": "Title 1",
      "content": "Quaerat sit dolorem velit...",
      "author": "Author 1"
    },
    {
      "id": 2,
      "title": "Title 2",
      "content": "Amet quiquia sed ut velit...",
      "author": "Author 2"
    }
  ]
}

Get a Single Post

v1 (Gin):

curl http://localhost:8080/api/v1/posts/1

v2 (Huma):

curl http://localhost:8080/api/v2/posts/1

Response Example:

{
  "result": {
    "id": 1,
    "title": "Title 1",
    "content": "Quaerat sit dolorem velit...",
    "author": "Author 1"
  }
}

Create a New Post

v1 (Gin):

curl -X POST http://localhost:8080/api/v1/posts \
  -H "Content-Type: application/json" \
  -d '{
    "title": "My New Post",
    "content": "This is the content of my post",
    "author": "John Doe"
  }'

v2 (Huma):

curl -X POST http://localhost:8080/api/v2/posts \
  -H "Content-Type: application/json" \
  -d '{
    "title": "My New Post",
    "content": "This is the content of my post",
    "author": "John Doe"
  }'

Response Example:

{
  "result": {
    "id": 101,
    "title": "My New Post",
    "content": "This is the content of my post",
    "author": "John Doe"
  }
}

Update a Post

v1 (Gin):

curl -X PUT http://localhost:8080/api/v1/posts/1 \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Updated Title",
    "content": "Updated content",
    "author": "Updated Author"
  }'

v2 (Huma):

curl -X PUT http://localhost:8080/api/v2/posts/1 \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Updated Title",
    "content": "Updated content",
    "author": "Updated Author"
  }'

Delete a Post

v1 (Gin):

curl -X DELETE http://localhost:8080/api/v1/posts/1

v2 (Huma):

curl -X DELETE http://localhost:8080/api/v2/posts/1

Error Handling Comparison

Invalid Request (v1 - Gin):

curl -X POST http://localhost:8080/api/v1/posts \
  -H "Content-Type: application/json" \
  -d '{
    "title": "",
    "content": "Some content",
    "author": "John"
  }'

Response:

{
  "error": "title can`t be empty"
}

Invalid Request (v2 - Huma):

curl -X POST http://localhost:8080/api/v2/posts \
  -H "Content-Type: application/json" \
  -d '{
    "title": "",
    "content": "Some content",
    "author": "John"
  }'

Response (RFC 9457 Problem Details):

{
  "$schema": "https://huma.rocks/schemas/problem",
  "title": "Bad Request",
  "status": 400,
  "detail": "title can`t be empty",
  "errors": [
    {
      "message": "title can`t be empty",
      "location": "body",
      "value": ""
    }
  ]
}

Notice how Huma's v2 API provides more structured error responses following RFC 9457 standard, making it easier for clients to handle errors programmatically.

Validation Examples

Field Length Validation:

# Title too long (>50 characters)
curl -X POST http://localhost:8080/api/v2/posts \
  -H "Content-Type: application/json" \
  -d '{
    "title": "This is a very long title that exceeds the maximum allowed length of fifty characters",
    "content": "Some content",
    "author": "John"
  }'

Special Characters Validation:

# Title contains prohibited characters
curl -X POST http://localhost:8080/api/v2/posts \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Title with @ special chars!",
    "content": "Some content",
    "author": "John"
  }'

Invalid ID Type (v2 only - automatic validation):

# v2 automatically validates that ID is a number
curl http://localhost:8080/api/v2/posts/invalid
# Returns 422 Unprocessable Entity with proper error message

# v1 requires manual validation in code
curl http://localhost:8080/api/v1/posts/invalid
# Returns error but less structured

Configuration

The application uses Viper for flexible configuration management. Configuration can be provided via:

  1. config.yaml file (recommended)
  2. Environment variables (override config.yaml values)
  3. Default values (used if no config file or env vars are set)

Configuration File

Copy the example configuration and customize it:

cp config.yaml.example config.yaml

Edit config.yaml:

# Server configuration
server:
  addr: "localhost:8080"
  url_origin: "http://localhost:8080"

# Data configuration
data:
  initial_data_file: "cmd/blog_data.json"

Configuration Options

YAML Path Default Description
server.addr localhost:8080 Server address and port
server.url_origin http://localhost:8080 CORS allowed origin
data.initial_data_file cmd/blog_data.json Path to initial blog data

Configuration Priority

Configuration is loaded in the following order (later sources override earlier ones):

  1. Default values (hardcoded in application)
  2. config.yaml file (if present)
  3. Environment variables (highest priority)

Configuration File Locations

The application searches for config.yaml in the following locations:

  1. Current working directory (.)
  2. ./config directory
  3. $HOME/.api-demo directory

Using Environment Variables

Environment variables automatically override config.yaml values. Convert YAML paths to uppercase and replace dots with underscores:

YAML path → Environment variable:

  • server.addrSERVER_ADDR
  • server.url_originSERVER_URL_ORIGIN
  • data.initial_data_fileDATA_INITIAL_DATA_FILE

Examples:

# Override server address
export SERVER_ADDR="localhost:3000"

# Override data file location
export DATA_INITIAL_DATA_FILE="/path/to/custom/data.json"

# Override CORS origin
export SERVER_URL_ORIGIN="http://localhost:3000"

# Run the application
make run

Note: Viper's AutomaticEnv() automatically maps environment variables to config keys. No explicit binding needed!

Running Without config.yaml

The application will work without a config.yaml file, using default values:

# Delete or rename config.yaml
mv config.yaml config.yaml.bak

# Run with defaults
make run
# Server will start on localhost:8080

License

MIT License - see LICENSE file for details.

Author

Copyright (c) 2024 Serhii Nesterenko

About

golang API server demo

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published