REST Package

REST Package

The rest package provides a comprehensive toolkit for building and consuming RESTful APIs in Go. It offers both client and server implementations with a rich set of features for building robust, production-ready web services.

Features

Client Features

  • HTTP Methods: Support for GET, POST, PUT, DELETE and other standard methods
  • Request Configuration: Easy setup of headers, query parameters, and body content
  • Resilience Features: Built-in retry and circuit breaker capabilities
  • TLS Configuration: Secure communication with custom certificates
  • Proxy Support: Configure proxies for HTTP requests
  • Transport Layer Configuration: Fine-tune connection timeouts and pooling

Server Features

  • Routing: Simple API for defining routes with different HTTP methods
  • Path Parameters: Support for route parameters (e.g., /users/:id)
  • Query Parameters: Easy access to query string parameters
  • Request Context: Context object that provides access to request data and response writing
  • Lifecycle Management: Integration with the lifecycle package for managing server startup and shutdown
  • TLS Support: Secure your API with HTTPS

Core Components

Client Components

  • Client: Main client object for executing HTTP requests
  • Request: Represents an HTTP request with its associated configuration
  • Response: Encapsulates the response from an HTTP request

Server Components

  • Server: Main server object for defining routes and handling requests
  • ServerContext: Context object provided to request handlers with access to request data and response writing
  • ServerOptions: Configuration options for the server (e.g., address, timeouts)

Usage Examples

Client Usage

Basic GET Request

package main

import (
    "fmt"
    "oss.nandlabs.io/golly/rest"
)

func main() {
    // Create a new client
    client := rest.NewClient()

    // Create a new request
    req := client.NewRequest("https://api.example.com/users", "GET")

    // Add headers
    req.AddHeader("Accept", "application/json")

    // Add query parameters
    req.AddParam("page", "1")
    req.AddParam("limit", "10")

    // Execute the request
    res, err := client.Execute(req)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }

    // Check status code
    if res.StatusCode() != 200 {
        fmt.Printf("Unexpected status code: %d\n", res.StatusCode())
        return
    }

    // Get the response body
    body := res.GetBody()
    fmt.Printf("Response: %s\n", string(body))
}

POST Request with JSON Body

package main

import (
    "encoding/json"
    "fmt"
    "oss.nandlabs.io/golly/rest"
)

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    // Create a new client
    client := rest.NewClient()

    // Create a new POST request
    req := client.NewRequest("https://api.example.com/users", "POST")

    // Set headers
    req.AddHeader("Content-Type", "application/json")
    req.AddHeader("Accept", "application/json")

    // Create and set the request body
    user := User{
        Name:  "John Doe",
        Email: "john@example.com",
    }

    userData, _ := json.Marshal(user)
    req.SetBody(userData)

    // Execute the request
    res, err := client.Execute(req)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }

    // Handle the response
    if res.StatusCode() == 201 {
        fmt.Println("User created successfully")
    } else {
        fmt.Printf("Error creating user: %s\n", string(res.GetBody()))
    }
}

Client with Retry and Circuit Breaker

package main

import (
    "fmt"
    "oss.nandlabs.io/golly/rest"
)

func main() {
    // Create a new client
    client := rest.NewClient()

    // Configure retry (max retries = 3, wait time = 5 seconds)
    client.Retry(3, 5)

    // Configure circuit breaker
    // (failure threshold = 3, success threshold = 2, max half-open = 1, timeout = 30 seconds)
    client.UseCircuitBreaker(3, 2, 1, 30)

    // Create and execute a request
    req := client.NewRequest("https://api.example.com/data", "GET")
    res, err := client.Execute(req)

    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }

    fmt.Printf("Response: %s\n", string(res.GetBody()))
}

Server Usage

Basic Server with Multiple Endpoints

package main

import (
    "encoding/json"
    "net/http"

    "oss.nandlabs.io/golly/lifecycle"
    "oss.nandlabs.io/golly/rest"
)

type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

func main() {
    // Create a new server
    srv, err := rest.DefaultServer()
    if err != nil {
        panic(err)
    }

    // Configure the server
    srv.Opts().PathPrefix = "/api/v1"

    // Define a GET endpoint
    srv.Get("users", func(ctx rest.ServerContext) {
        // Mock data
        users := []User{
            {ID: "1", Name: "Alice"},
            {ID: "2", Name: "Bob"},
        }

        // Convert to JSON
        data, err := json.Marshal(users)
        if err != nil {
            ctx.SetStatusCode(http.StatusInternalServerError)
            ctx.WriteString("Error marshaling data")
            return
        }

        // Set response headers
        ctx.SetHeader("Content-Type", "application/json")

        // Write response
        ctx.SetStatusCode(http.StatusOK)
        ctx.Write(data)
    })

    // Define a GET endpoint with path parameter
    srv.Get("users/:id", func(ctx rest.ServerContext) {
        // Get path parameter
        id := ctx.GetParam("id", rest.PathParam)

        // Mock data retrieval
        var user User
        if id == "1" {
            user = User{ID: "1", Name: "Alice"}
        } else if id == "2" {
            user = User{ID: "2", Name: "Bob"}
        } else {
            ctx.SetStatusCode(http.StatusNotFound)
            ctx.WriteString("User not found")
            return
        }

        // Convert to JSON
        data, err := json.Marshal(user)
        if err != nil {
            ctx.SetStatusCode(http.StatusInternalServerError)
            ctx.WriteString("Error marshaling data")
            return
        }

        // Set response
        ctx.SetHeader("Content-Type", "application/json")
        ctx.SetStatusCode(http.StatusOK)
        ctx.Write(data)
    })

    // Define a POST endpoint
    srv.Post("users", func(ctx rest.ServerContext) {
        // Get request body
        body, err := ctx.GetBody()
        if err != nil {
            ctx.SetStatusCode(http.StatusBadRequest)
            ctx.WriteString("Error reading request body")
            return
        }

        // Parse the user data
        var newUser User
        err = json.Unmarshal(body, &newUser)
        if err != nil {
            ctx.SetStatusCode(http.StatusBadRequest)
            ctx.WriteString("Invalid user data")
            return
        }

        // In a real app, you would save the user here

        // Return the created user
        ctx.SetHeader("Content-Type", "application/json")
        ctx.SetStatusCode(http.StatusCreated)
        ctx.Write(body)
    })

    // Create a lifecycle manager
    mgr := lifecycle.NewSimpleComponentManager()

    // Register the server with the lifecycle manager
    mgr.Register(srv)

    // Start the server and wait for signals
    mgr.StartAndWait()
}

Server with Custom Configuration

package main

import (
    "net/http"

    "oss.nandlabs.io/golly/lifecycle"
    "oss.nandlabs.io/golly/rest"
)

func main() {
    // Create server options
    opts := &rest.SrvOptions{
        Host:         "localhost",
        Port:         8443,
        ReadTimeout:  30,  // seconds
        WriteTimeout: 30,  // seconds
        UseTLS:       true,
        CertFile:     "server.crt",
        KeyFile:      "server.key",
        PathPrefix:   "/api/v2",
    }

    // Create a new server with custom options
    srv, err := rest.New(opts)
    if err != nil {
        panic(err)
    }

    // Add middleware-like functionality
    srv.Before(func(ctx rest.ServerContext) bool {
        // Log all requests
        fmt.Printf("Request: %s %s\n", ctx.Method(), ctx.Path())

        // Check authentication
        authHeader := ctx.GetHeader("Authorization")
        if authHeader == "" {
            ctx.SetStatusCode(http.StatusUnauthorized)
            ctx.WriteString("Authentication required")
            return false  // Stop handling
        }

        return true  // Continue handling
    })

    // Add routes
    srv.Get("healthz", func(ctx rest.ServerContext) {
        ctx.SetStatusCode(http.StatusOK)
        ctx.WriteString("OK")
    })

    // Create lifecycle manager and start
    mgr := lifecycle.NewSimpleComponentManager()
    mgr.Register(srv)
    mgr.StartAndWait()
}

Installation

go get oss.nandlabs.io/golly/rest