How to build APIs in Go: create a statistical dashboard with Gin-Gonic
How to build APIs in Go: create a statistical dashboard with Gin-Gonic
Table of Contents
- Introduction
- Why Gin for Building APIs?
- Project Setup
- Building Our First Endpoint
- Understanding Routing and HTTP Methods
- Request Handling and Validation
Introduction
In this tutorial, we'll build a Statistical Reporting API using Gin, one of the fastest and most popular web frameworks for Go. Our application will serve various statistical reports with mock data, demonstrating real-world API development patterns.
What We'll Build:
- A RESTful API for statistical reports
- Endpoints for user statistics, sales data, and performance metrics
- Proper error handling and validation
Why Gin for Building APIs?
Gin-Gonic is a high-performance HTTP web framework written in Go. Here's why it's excellent for API development:
Performance
- 40x faster than other Go frameworks in many benchmarks
- Uses httprouter under the hood for efficient routing
- Minimal memory allocation and overhead
Developer Experience
- Clean, intuitive API
- Excellent middleware support
- Built-in validation and rendering
- Great documentation and community
Production-Ready Features
- Crash-free operation with panic recovery
- JSON validation
- Route grouping
- Error management
Project Setup
Step 1: Initialize Your Go Module
First, create a new directory:
mkdir api
Note: Usually when we create a new Go project we need to run go mod init <name> to initialize the new module, but if we are executing the code in this lesson
in the Bytestark Golang sandbox we don't need to do that as the main module is already pre-initialized. All you need to do is to add your project files and leverage existing mod file.
Step 3: Create Project Structure
mkdir -p api/handlers
mkdir -p api/models
Why this structure?
- main.go: Contains the main application entry point
- api/handlers: HTTP request handlers (controllers)
- api/models: Data structures and business logic
Building Our First Endpoint
Let's start with the simplest possible API - a health check endpoint.
Create main.go
The default working directory when using a Bytestark sandbox is /home/coder/learning.
If you are running this code in Go sandbox on Bytestark make sure your current directory is /home/coder/learning.
To validate this you can open a terminal and execute the command:
pwd
If the result is
`/home/coder/learning`
then we are good to proceed to the next step.
Create main.go:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
// Create a new Gin router with default middleware
router := gin.Default()
// Define our first endpoint
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"message": "API is running",
})
})
// Start the server on port 8080
router.Run(":8080")
}
Code Explanation
Let's break down each part:
1. router := gin.Default()
What it does:
- Creates a new Gin engine instance
- Includes default middleware: Logger and Recovery
Why we need it:
- Logger middleware logs all HTTP requests
- Recovery middleware catches panics and returns 500 errors instead of crashing
Alternative:
router := gin.New() // Creates router without any middleware
2. router.GET("/health", func(c *gin.Context) {...})
What it does:
- Registers a route that responds to GET requests at /health
- The anonymous function is the handler - it processes the request
Parameters:
- /health: The URL path
- func(c *gin.Context): Handler function receiving Gin's context
3. c *gin.Context
What it is:
- The most important type in Gin
- Contains everything about the current HTTP request and response
- Provides methods to read request data and write responses
Key methods:
- c.JSON(): Send JSON response
- c.Param(): Get URL parameters
- c.Query(): Get query string parameters
- c.Bind(): Parse request body
4. c.JSON(http.StatusOK, gin.H{...})
What it does:
- Sets response status code to 200 (OK)
- Serializes the data to JSON
- Sets Content-Type: application/json header
- Writes the response
gin.H:
- A shortcut for map[string]interface{}
- Makes it easy to create JSON responses
5. router.Run(":8080")
What it does:
- Starts the HTTP server
- Listens on port 8080
- Blocks until the server is stopped
Running the API
In your terminal run:
go run main.go
You should see in your terminal the following output:
[GIN-debug] GET /health --> main.main.func1 (3 handlers)
[GIN-debug] Listening and serving HTTP on :8080
What this means:
- Gin is in debug mode (shows routing information)
- One route registered: GET /health
- Server is listening on localhost:8080
Testing the Endpoint
When your development server starts it will start listening on the port 8080 and VSCode will display a message and a button, offering you a convenient way to open a new
browser window with the URL where you can access your application.
Alternatively, you can open a new browser tab yourself and access the URL of your sandbox at /proxy/8080/health
You should see the following response
{
"status": "healthy",
"message": "API is running"
}
which indicates your API has started and is working correctly.
What Happens Behind the Scenes
- Request arrives at localhost:8080/health
- Gin's router matches the request to the
/healthroute - Logger middleware logs the request
- Handler function executes
- JSON response is serialized and sent
- Logger middleware logs the response status and time
Understanding Routing and HTTP Methods
Now let's expand our API with proper routing patterns.
HTTP Methods in REST
REST APIs use HTTP methods to indicate operations:
- GET: Retrieve data (read-only)
- POST: Create new resources
- PUT: Update entire resources
- PATCH: Partially update resources
- DELETE: Remove resources
Create models/stats.go
First, let's define our data structures in api/models/stats.go:
package models
import "time"
// UserStats represents user activity statistics
type UserStats struct {
TotalUsers int `json:"total_users"`
ActiveUsers int `json:"active_users"`
NewUsers int `json:"new_users"`
Timestamp time.Time `json:"timestamp"`
}
// SalesReport represents sales data for a period
type SalesReport struct {
Period string `json:"period"`
TotalSales float64 `json:"total_sales"`
TotalOrders int `json:"total_orders"`
AverageOrder float64 `json:"average_order"`
TopProduct string `json:"top_product"`
}
// PerformanceMetrics represents system performance data
type PerformanceMetrics struct {
CPUUsage float64 `json:"cpu_usage"`
MemoryUsage float64 `json:"memory_usage"`
RequestsPerSec int `json:"requests_per_sec"`
AvgResponseTime float64 `json:"avg_response_time_ms"`
}
Code Explanation: Models
Struct Tags json:"field_name"
What it does:
- Controls how struct fields are serialized to JSON
- The field name in JSON will be "total_users", not "TotalUsers"
Why lowercase?
- JavaScript convention uses camelCase/snake_case
- Makes API responses consistent with frontend expectations
Example:
// Without tags:
{"TotalUsers": 100}
// With tags:
{"total_users": 100}
Create handlers/handlers.go
Now create api/handlers/handlers.go:
package handlers
import (
"math/rand"
"net/http"
"learning/api/models"
"time"
"github.com/gin-gonic/gin"
)
// StatsHandler holds dependencies for stats-related handlers
type StatsHandler struct {
// In a real app, this would include database connections, etc.
}
// NewStatsHandler creates a new stats handler instance
func NewStatsHandler() *StatsHandler {
return &StatsHandler{}
}
// GetUserStats returns current user statistics
func (h *StatsHandler) GetUserStats(c *gin.Context) {
// Generate mock data
stats := models.UserStats{
TotalUsers: rand.Intn(10000) + 5000,
ActiveUsers: rand.Intn(3000) + 1000,
NewUsers: rand.Intn(500) + 100,
Timestamp: time.Now(),
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": stats,
})
}
Code Explanation: Handlers
1. Handler Struct Pattern
type StatsHandler struct {}
Why use a struct?
- Allows dependency injection
- Easy to add database connections, services, etc.
- Makes testing easier (can mock dependencies)
Example with dependencies:
type StatsHandler struct {
db *sql.DB
cache *redis.Client
logger *log.Logger
}
2. Constructor Function
func NewStatsHandler() *StatsHandler {
return &StatsHandler{}
}
What it does:
- Creates and returns a new handler instance
- Common Go pattern for initialization
Why return a pointer?
- Efficient (doesn't copy the struct)
- Allows methods to modify handler state
3. Method Receiver
func (h *StatsHandler) GetUserStats(c *gin.Context) {...}
What (h *StatsHandler) means:
- This function is a method on StatsHandler
- h is the receiver (like this or self in other languages)
- Can access handler's fields via h.db, etc.
4. Response Wrapper Pattern
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": stats,
})
Why wrap responses?
- Consistent API response format
- Easy to add metadata (pagination, errors, etc.)
- Frontend knows what to expect
Response structure:
{
"success": true,
"data": {
"total_users": 7543,
"active_users": 2341,
"new_users": 287,
"timestamp": "2025-11-10T10:30:00Z"
}
}
Update main.go with Routing
Update main.go:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"learning/api/handlers"
)
func main() {
router := gin.Default()
// Initialize handlers
statsHandler := handlers.NewStatsHandler()
// Health check endpoint
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"message": "API is running",
})
})
// API v1 routes group
v1 := router.Group("/api/v1")
{
// Stats endpoints
stats := v1.Group("/stats")
{
stats.GET("/users", statsHandler.GetUserStats)
}
}
router.Run(":8080")
}
Code Explanation: Route Groups
1. router.Group("/api/v1")
What it does:
- Creates a route group with the prefix /api/v1
- All routes added to this group will start with /api/v1
Why use groups?
- API versioning (/api/v1, /api/v2)
- Organize related endpoints
- Apply middleware to specific routes
2. Nested Groups
v1 := router.Group("/api/v1")
{
stats := v1.Group("/stats")
{
stats.GET("/users", handler)
}
}
Final URL: /api/v1/stats/users
Benefits:
- Clear hierarchical structure
- Easy to reorganize
- Self-documenting code
3. The Curly Braces {}
What they do:
- Create a code block (scope)
- Visual organization only
- Not required but improves readability
Running and Testing
Start the server:
Note: If your server is currently running you need to stop it with Ctrl+C, rebuild the API and restart it using:
go run main.go
Test the new endpoint by accessing the new URL in your browser:
/api/v1/stats/users
Expected response:
{
"success": true,
"data": {
"total_users": 7543,
"active_users": 2341,
"new_users": 287,
"timestamp": "2025-11-10T10:30:00.000Z"
}
}
Request Handling and Validation
Now let's add endpoints that accept parameters and request bodies.
URL Parameters vs Query Parameters
URL Parameters (Path Parameters):
- Part of the URL path: /user/:id
- Example: /user/123
- Use for identifying specific resources
Query Parameters:
- After the ? in URL: /search?q=golang
- Example: /reports?period=monthly&year=2025
- Use for filtering, sorting, pagination
Add this code to handlers/handlers.go
// GetSalesReport returns sales data for a specific period
func (h *StatsHandler) GetSalesReport(c *gin.Context) {
// Get query parameter
period := c.DefaultQuery("period", "monthly")
// Validate period
validPeriods := map[string]bool{
"daily": true,
"weekly": true,
"monthly": true,
"yearly": true,
}
if !validPeriods[period] {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"error": "Invalid period. Use: daily, weekly, monthly, or yearly",
})
return
}
// Generate mock data based on period
totalSales := rand.Float64() * 100000
totalOrders := rand.Intn(1000) + 100
report := models.SalesReport{
Period: period,
TotalSales: totalSales,
TotalOrders: totalOrders,
AverageOrder: totalSales / float64(totalOrders),
TopProduct: "Product-" + string(rune(rand.Intn(26)+65)),
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": report,
})
}
// GetPerformanceMetrics returns system performance data
func (h *StatsHandler) GetPerformanceMetrics(c *gin.Context) {
metrics := models.PerformanceMetrics{
CPUUsage: rand.Float64() * 100,
MemoryUsage: rand.Float64() * 100,
RequestsPerSec: rand.Intn(1000) + 100,
AvgResponseTime: rand.Float64() * 100,
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": metrics,
})
}
Code Explanation: Query Parameters
1. c.DefaultQuery("period", "monthly")
What it does:
- Retrieves the "period" query parameter
- Returns "monthly" if parameter is missing
Examples:
/sales?period=weekly → period = "weekly"
/sales → period = "monthly" (default)
Alternative:
period := c.Query("period") // Returns empty string if missing
2. Validation Pattern
validPeriods := map[string]bool{
"daily": true,
"weekly": true,
}
if !validPeriods[period] {
c.JSON(http.StatusBadRequest, ...)
return
}
Why this pattern?
- Fast lookup using map
- Clear list of valid values
- Easy to maintain
Important: Always return after sending an error response!
3. Error Response Structure
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"error": "Invalid period...",
})
HTTP Status Code Examples:
- 200 OK: Success
- 400 Bad Request: Client error (invalid input)
- 404 Not Found: Resource doesn't exist
- 500 Internal Server Error: Server error
Add URL Parameter Handling
Add to handlers/handlers.go:
// GetUserStatsByID returns stats for a specific user
func (h *StatsHandler) GetUserStatsByID(c *gin.Context) {
// Get URL parameter
userID := c.Param("id")
// In a real app, you'd query the database
// For now, we'll validate and return mock data
if userID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"error": "User ID is required",
})
return
}
// Mock user stats
stats := gin.H{
"user_id": userID,
"total_orders": rand.Intn(100),
"total_spent": rand.Float64() * 1000,
"account_age_days": rand.Intn(365),
"last_login": time.Now().Add(-time.Duration(rand.Intn(24)) * time.Hour),
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": stats,
})
}
Code Explanation: URL Parameters
c.Param("id")
What it does:
- Extracts the :id parameter from the URL path
- Returns empty string if parameter doesn't exist
Route definition:
router.GET("/user/:id", handler)
URL examples:
/user/123 → id = "123"
/user/abc-456 → id = "abc-456"
/user/ → doesn't match route (404)
Multiple parameters:
router.GET("/user/:userID/orders/:orderID", handler)
// Access:
userID := c.Param("userID")
orderID := c.Param("orderID")
Update main.go with New Routes
Update main.go:
func main() {
router := gin.Default()
statsHandler := handlers.NewStatsHandler()
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
})
})
v1 := router.Group("/api/v1")
{
stats := v1.Group("/stats")
{
stats.GET("/users", statsHandler.GetUserStats)
stats.GET("/user/:id", statsHandler.GetUserStatsByID)
stats.GET("/sales", statsHandler.GetSalesReport)
stats.GET("/performance", statsHandler.GetPerformanceMetrics)
}
}
router.Run(":8080")
}
Running and Testing Multiple Endpoints
Start the server:
go run main.go
Test 1: User stats (no parameters)
/api/v1/stats/users
Test 2: Specific user (URL parameter)
/api/v1/stats/user/12345
Expected response:
{
"success": true,
"data": {
"user_id": "12345",
"total_orders": 47,
"total_spent": 532.89,
"account_age_days": 180,
"last_login": "2025-11-09T14:22:00Z"
}
}
Test 3: Sales report (query parameter)
/api/v1/stats/sales?period=weekly"
Note: Use quotes when URL contains special characters like ?
Expected response:
{
"success": true,
"data": {
"period": "weekly",
"total_sales": 87543.21,
"total_orders": 543,
"average_order": 161.22,
"top_product": "Product-F"
}
}
Test 4: Invalid query parameter
/api/v1/stats/sales?period=invalid"
Expected response:
{
"success": false,
"error": "Invalid period. Use: daily, weekly, monthly, or yearly"
}
Test 5: Performance metrics
/api/v1/stats/performance
What We've learned
Congratulations! You've successfully built a functional RESTful API using Gin-Gonic. Let's recap what you've learned and implemented:
Gin Fundamentals
- Setting up a Gin router with
gin.Default() - Understanding the Gin Context (
*gin.Context) - Creating JSON responses with
gin.H - Starting the HTTP server
- Setting up a Gin router with
Routing Patterns
- Defining routes with HTTP methods (GET, POST, etc.)
- Creating route groups for better organization
- Understanding URL structure and API versioning
- Nested route groups for hierarchical endpoints
Request Handling
- Extracting URL parameters with
c.Param() - Reading query parameters with
c.Query()andc.DefaultQuery() - Validating input data
- Sending appropriate error responses with HTTP status codes
- Extracting URL parameters with
Data Modeling
- Creating Go structs to represent data
- Using struct tags for JSON serialization
- Organizing models in a separate package
- Generating mock data for testing
Handler Organization
- Using the handler struct pattern
- Creating constructor functions
- Method receivers for clean code organization
- Consistent response wrapper patterns
You now have a Statistical Reporting API with these functional endpoints:
GET /health - Health check
GET /api/v1/stats/users - All user statistics
GET /api/v1/stats/user/:id - Specific user statistics
GET /api/v1/stats/sales - Sales report (with period filter)
GET /api/v1/stats/performance - Performance metrics
English
Română