Go Formatter API with Gin + gofmt Integration | Web Formatter Blog

Go Formatter API with Gin + gofmt Integration
A comprehensive guide to building a Go code formatting HTTP service using Gin and gofmt.
Introduction
Consistent code formatting is essential for maintaining readable
and maintainable codebases. Go, with its emphasis on simplicity
and readability, provides a built-in tool called{" "}
gofmt
that automatically formats Go source code
according to a standardized style.
In this guide, we'll explore how to build a robust HTTP API
service using the Gin framework that leverages
gofmt
to provide code formatting as a service. This
can be particularly useful for integrating with editors, CI/CD
pipelines, or providing a web-based formatting tool.
Why Build a Go Formatter API?
There are several compelling reasons to build a dedicated Go formatter API:
- Centralized formatting service: Provide consistent formatting across teams and projects
- Web-based formatting tools: Enable formatting without local Go installation
- Editor integrations: Allow code editors to format Go code via HTTP requests
- CI/CD integration: Validate code formatting as part of automated pipelines
- Custom formatting rules: Extend gofmt with additional formatting options
- Analytics and monitoring: Track formatting usage and patterns
Prerequisites
Before we begin, ensure you have the following installed:
- Go (version 1.16 or later)
- Git
- Basic understanding of RESTful APIs
- Familiarity with Go programming
You can verify your Go installation with:
go version
For local development, you'll want to have Go installed. Visit{" "} go.dev/dl {" "} > follow the installation instructions for your operating system.
Project Setup
Setting Up Gin Framework
Let's start by creating a new Go module and installing the Gin framework:
mkdir go-formatter-api
cd go-formatter-api
go mod init github.com/yourusername/go-formatter-api
go get -u github.com/gin-gonic/gin
Gin is a high-performance HTTP web framework written in Go. It provides a martini-like API with much better performance, up to 40 times faster.
Project Structure
Let's organize our project with a clean structure:
go-formatter-api/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── api/
│ │ ├── handlers/
│ │ │ └── formatter.go
│ │ ├── middleware/
│ │ │ ├── logger.go
│ │ │ └── ratelimit.go
│ │ └── routes/
│ │ └── routes.go
│ └── formatter/
│ └── service.go
├── pkg/
│ └── utils/
│ └── response.go
├── go.mod
├── go.sum
└── README.md
This structure follows Go best practices by separating concerns:
-
cmd/
: Contains the application entry points -
internal/
: Private application code -
pkg/
: Code that can be used by external applications -
api/
: API-related code (handlers, middleware, routes) -
formatter/
: Core formatting service logic
Building the Formatter Service
Integrating gofmt
Let's create our formatter service that will integrate with the{" "}
gofmt
tool:
// internal/formatter/service.go
package formatter
import (
"bytes"
"errors"
"os/exec"
"strings"
)
// Service provides Go code formatting functionality
type Service struct {
// Configuration options could be added here
}
// NewService creates a new formatter service
func NewService() *Service {
return &Service{}
}
// FormatCode formats Go code using gofmt
func (s *Service) FormatCode(code string) (string, error) {
// Create a new gofmt command
cmd := exec.Command("gofmt")
// Provide the input code
cmd.Stdin = strings.NewReader(code)
// Capture stdout and stderr
var out, stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
// Run the command
err := cmd.Run()
if err != nil {
// If there's an error, return the stderr output
return "", errors.New(stderr.String())
}
// Return the formatted code
return out.String(), nil
}
// ValidateCode checks if the provided code is valid Go code
func (s *Service) ValidateCode(code string) error {
// Create a new gofmt command with -e flag (report errors only)
cmd := exec.Command("gofmt", "-e")
// Provide the input code
cmd.Stdin = strings.NewReader(code)
// Capture stderr
var stderr bytes.Buffer
cmd.Stderr = &stderr
// Run the command
err := cmd.Run()
if err != nil {
// If there's an error, return the stderr output
return errors.New(stderr.String())
}
return nil
}
This service provides two main functions:
-
FormatCode
: Formats Go code using gofmt -
ValidateCode
: Validates if the provided code is valid Go code
Formatter Logic Implementation
We can extend our formatter service with additional options:
// internal/formatter/service.go
// Add to the existing file
// FormatOptions represents options for formatting
type FormatOptions struct {
Simplify bool // -s flag: simplify code
Rewrite bool // -r flag: apply rewrite rule
Rule string // rewrite rule when Rewrite is true
}
// FormatCodeWithOptions formats Go code with specific options
func (s *Service) FormatCodeWithOptions(code string, options FormatOptions) (string, error) {
// Start with basic gofmt command
args := []string{}
// Add options
if options.Simplify {
args = append(args, "-s")
}
if options.Rewrite && options.Rule != "" {
args = append(args, "-r", options.Rule)
}
// Create command with arguments
cmd := exec.Command("gofmt", args...)
// Provide the input code
cmd.Stdin = strings.NewReader(code)
// Capture stdout and stderr
var out, stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
// Run the command
err := cmd.Run()
if err != nil {
return "", errors.New(stderr.String())
}
// Return the formatted code
return out.String(), nil
}
This extended function allows us to use additional gofmt options:
-
-s
: Simplifies code (e.g., removes redundant type declarations) -
-r
: Applies rewrite rules (e.g.,{" "}a[b:len(a)] {"->"} a[b:]
)
Creating API Endpoints
Format Endpoint
Now, let's create the API handler for our formatting endpoint:
// internal/api/handlers/formatter.go
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/yourusername/go-formatter-api/internal/formatter"
)
// FormatRequest represents the request body for formatting
type FormatRequest struct {
Code string `json:"code" binding:"required"`
Options formatter.FormatOptions `json:"options"`
}
// FormatResponse represents the response for formatting
type FormatResponse struct {
FormattedCode string `json:"formatted_code"`
}
// FormatterHandler handles formatting requests
type FormatterHandler struct {
service *formatter.Service
}
// NewFormatterHandler creates a new formatter handler
func NewFormatterHandler(service *formatter.Service) *FormatterHandler {
return &FormatterHandler{
service: service,
}
}
// FormatCode handles the code formatting request
func (h *FormatterHandler) FormatCode(c *gin.Context) {
var req FormatRequest
// Bind JSON request
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid request: " + err.Error(),
})
return
}
// Validate the code
if err := h.service.ValidateCode(req.Code); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid Go code: " + err.Error(),
})
return
}
// Format the code with options
formattedCode, err := h.service.FormatCodeWithOptions(req.Code, req.Options)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Formatting failed: " + err.Error(),
})
return
}
// Return the formatted code
c.JSON(http.StatusOK, FormatResponse{
FormattedCode: formattedCode,
})
}
Health Check Endpoint
Let's add a health check endpoint to verify our API is running:
// internal/api/handlers/health.go
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
)
// HealthHandler handles health check requests
type HealthHandler struct{}
// NewHealthHandler creates a new health handler
func NewHealthHandler() *HealthHandler {
return &HealthHandler{}
}
// Check handles the health check request
func (h *HealthHandler) Check(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"service": "go-formatter-api",
})
}
Request Validation
We can improve our request validation by adding custom validators:
// internal/api/handlers/formatter.go
// Add to the existing file
// ValidateRequest validates the format request
func (h *FormatterHandler) ValidateRequest(c *gin.Context) {
var req FormatRequest
// Check if request body is valid JSON
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid JSON: " + err.Error(),
})
c.Abort()
return
}
// Check if code is empty
if strings.TrimSpace(req.Code) == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Code cannot be empty",
})
c.Abort()
return
}
// Store the parsed request in context for later use
c.Set("formatRequest", req)
c.Next()
}
Error Handling
Let's implement a centralized error handling approach:
// pkg/utils/response.go
package utils
import (
"github.com/gin-gonic/gin"
)
// ErrorResponse represents a standard error response
type ErrorResponse struct {
Error string `json:"error"`
Details string `json:"details,omitempty"`
Code string `json:"code,omitempty"`
}
// RespondWithError sends a standardized error response
func RespondWithError(c *gin.Context, status int, message string, details string, code string) {
c.JSON(status, ErrorResponse{
Error: message,
Details: details,
Code: code,
})
}
// RespondWithSuccess sends a standardized success response
func RespondWithSuccess(c *gin.Context, status int, data interface{}) {
c.JSON(status, gin.H{
"success": true,
"data": data,
})
}
Now we can update our handlers to use these utility functions:
// internal/api/handlers/formatter.go
// Update the FormatCode function
// FormatCode handles the code formatting request
func (h *FormatterHandler) FormatCode(c *gin.Context) {
var req FormatRequest
// Bind JSON request
if err := c.ShouldBindJSON(&req); err != nil {
utils.RespondWithError(c, http.StatusBadRequest, "Invalid request", err.Error(), "INVALID_REQUEST")
return
}
// Validate the code
if err := h.service.ValidateCode(req.Code); err != nil {
utils.RespondWithError(c, http.StatusBadRequest, "Invalid Go code", err.Error(), "INVALID_CODE")
return
}
// Format the code with options
formattedCode, err := h.service.FormatCodeWithOptions(req.Code, req.Options)
if err != nil {
utils.RespondWithError(c, http.StatusInternalServerError, "Formatting failed", err.Error(), "FORMAT_ERROR")
return
}
// Return the formatted code
utils.RespondWithSuccess(c, http.StatusOK, FormatResponse{
FormattedCode: formattedCode,
})
}
Middleware Implementation
Logging Middleware
Let's implement a logging middleware to track API requests:
// internal/api/middleware/logger.go
package middleware
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
// Logger is a middleware that logs request details
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
// Start timer
start := time.Now()
path := c.Request.URL.Path
// Process request
c.Next()
// Calculate latency
latency := time.Since(start)
// Get status code
statusCode := c.Writer.Status()
// Log request details
fmt.Printf("[%s] %s %s %d %s\n",
time.Now().Format(time.RFC3339),
c.Request.Method,
path,
statusCode,
latency,
)
}
}
Rate Limiting
To protect our API from abuse, let's add rate limiting:
// internal/api/middleware/ratelimit.go
package middleware
import (
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/yourusername/go-formatter-api/pkg/utils"
)
// RateLimiter implements a simple rate limiting middleware
type RateLimiter struct {
// Maps IP addresses to last request times
clients map[string][]time.Time
// Maximum requests per minute
limit int
// Mutex for concurrent access
mu sync.Mutex
}
// NewRateLimiter creates a new rate limiter
func NewRateLimiter(limit int) *RateLimiter {
return &RateLimiter{
clients: make(map[string][]time.Time),
limit: limit,
}
}
// Limit is a middleware that limits request rate
func (rl *RateLimiter) Limit() gin.HandlerFunc {
return func(c *gin.Context) {
// Get client IP
ip := c.ClientIP()
rl.mu.Lock()
// Clean old requests (older than 1 minute)
now := time.Now()
if times, exists := rl.clients[ip]; exists {
var newTimes []time.Time
for _, t := range times {
if now.Sub(t) < time.Minute {
newTimes = append(newTimes, t)
}
}
rl.clients[ip] = newTimes
}
// Check if client has exceeded rate limit
if len(rl.clients[ip]) >= rl.limit {
rl.mu.Unlock()
utils.RespondWithError(c, http.StatusTooManyRequests,
"Rate limit exceeded",
fmt.Sprintf("Maximum %d requests per minute allowed", rl.limit),
"RATE_LIMIT_EXCEEDED")
c.Abort()
return
}
// Add current request time
rl.clients[ip] = append(rl.clients[ip], now)
rl.mu.Unlock()
c.Next()
}
}
CORS Configuration
To allow cross-origin requests, let's add CORS middleware:
// internal/api/middleware/cors.go
package middleware
import (
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"time"
)
// CORS returns a middleware for handling CORS
func CORS() gin.HandlerFunc {
return cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
})
}
Testing the API
Unit Tests
Let's write unit tests for our formatter service:
// internal/formatter/service_test.go
package formatter
import (
"strings"
"testing"
)
func TestFormatCode(t *testing.T) {
service := NewService()
tests := []struct {
name string
input string
expected string
hasError bool
}{
{
name: "Valid code",
input: "package main\n\nfunc main(){\nfmt.Println(\"Hello, World!\")\n}",
expected: "package main\n\nfunc main() {\n\tfmt.Println(\"Hello, World!\")\n}\n",
hasError: false,
},
{
name: "Invalid code",
input: "package main\n\nfunc main() {\nfmt.Println(\"Missing closing parenthesis\"\n}",
expected: "",
hasError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := service.FormatCode(tt.input)
if tt.hasError && err == nil {
t.Errorf("Expected error but got none")
}
if !tt.hasError && err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !tt.hasError && result != tt.expected {
t.Errorf("Expected:\n%s\nGot:\n%s", tt.expected, result)
}
})
}
}
Integration Tests
Let's write integration tests for our API endpoints:
// internal/api/handlers/formatter_test.go
package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/yourusername/go-formatter-api/internal/formatter"
)
func setupRouter() *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.Default()
service := formatter.NewService()
handler := NewFormatterHandler(service)
r.POST("/format", handler.FormatCode)
return r
}
func TestFormatCodeEndpoint(t *testing.T) {
router := setupRouter()
tests := []struct {
name string
requestBody map[string]interface{}
expectedCode int
checkBody bool
}{
{
name: "Valid request",
requestBody: map[string]interface{}{
"code": "package main\n\nfunc main(){\nfmt.Println(\"Hello\")\n}",
"options": map[string]interface{}{
"simplify": true,
},
},
expectedCode: http.StatusOK,
checkBody: true,
},
{
name: "Empty code",
requestBody: map[string]interface{}{
"code": "",
},
expectedCode: http.StatusBadRequest,
checkBody: false,
},
{
name: "Invalid code",
requestBody: map[string]interface{}{
"code": "package main\n\nfunc main() {\nfmt.Println(\"Missing closing parenthesis\"\n}",
},
expectedCode: http.StatusBadRequest,
checkBody: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create request body
jsonBody, _ := json.Marshal(tt.requestBody)
req, _ := http.NewRequest("POST", "/format", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
// Create response recorder
w := httptest.NewRecorder()
// Perform request
router.ServeHTTP(w, req)
// Check status code
if w.Code != tt.expectedCode {
t.Errorf("Expected status %d but got %d", tt.expectedCode, w.Code)
}
// Check response body for successful requests
if tt.checkBody {
var response map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &response)
if response["formatted"] == nil {
t.Errorf("Expected formatted code in response")
}
}
})
}
}
Deployment
Once your API is tested and ready, you can deploy it to production. Let's look at deployment options.
Containerization with Docker
Create a Dockerfile for your application:
FROM golang:1.20-alpine AS builder
WORKDIR /app
# Copy go mod and sum files
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o go-formatter-api ./cmd/api
# Use a smaller image for the final application
FROM alpine:3.17
WORKDIR /app
# Copy the binary from the builder stage
COPY --from=builder /app/go-formatter-api .
# Expose the port
EXPOSE 8080
# Command to run
ENTRYPOINT ["./go-formatter-api"]
And a docker-compose.yml file for local development:
version: '3'
services:
api:
build: .
ports:
- "8080:8080"
environment:
- GIN_MODE=release
- PORT=8080
restart: unless-stopped
Cloud Deployment
You can deploy your containerized application to various cloud platforms:
- Google Cloud Run: Serverless container platform that automatically scales
- AWS Elastic Container Service: Managed container orchestration service
- DigitalOcean App Platform: Simple PaaS for containerized applications
- Kubernetes: For more complex deployments requiring orchestration
Conclusion
In this guide, we've built a Go formatting API using Gin and the standard gofmt tool. This API provides:
- A RESTful endpoint for formatting Go code
- Customizable formatting options
- Error handling and validation
- Rate limiting for API protection
- Comprehensive testing
This formatter-as-a-service can be integrated into various workflows:
- Web-based code editors
- CI/CD pipelines for code quality checks
- Development tools and plugins
- Learning platforms for Go programming
By following this guide, you've built a powerful Go code formatting API that leverages the strengths of Go, Gin, and gofmt to provide a valuable service for developers. Whether you deploy this as an internal tool or a public API, you've created a solution that can help maintain code consistency and improve developer workflows.