Let's walk through how to build a lightweight JSON server using only the Go standard library. This server will serve preloaded JSON data from either a local file or remote URL and support basic CRUD operations via HTTP methods like GET, POST, PUT, and DELETE. We’ll also add rate limiting and CORS support for robustness.
🚀 Overview
This server is designed to be minimal, fast, and flexible. It can be useful for:
- Mocking APIs during frontend development
 - Serving static JSON data with dynamic access
 - Learning how to work with Go's 
net/httppackage - Implementing middleware patterns (like rate limiting and CORS)
 
📁 Project Structure
We're using just one Go file that does all the heavy lifting:
- Loads JSON data at startup
 - Sets up an HTTP server
 - Handles routing
 - Implements middleware
 - Supports basic API functionality
 
Let’s dive into each part.
🧱 Data Model
First, we define our custom struct — representing the shape of our JSON data. In this example, it's a simple structure:
type JSONMODEL struct {
    Name     string `json:"name"`
    ID       int32  `json:"id"`
    PASSWORD string `json:"password"`
}
💡 Replace this with your own data model based on your JSON schema. Tools like quickjson.io can help generate structs from raw JSON.
We then declare a global variable to hold our loaded data:
var jsonData []JSONMODEL
🔁 Loading Data
The server supports loading data from two sources:
- A local file path
 - A remote URL
 
We determine the source type by checking if it starts with "http":
func loadData(source string) ([]JSONMODEL, error) {
    var reader io.Reader
    if strings.HasPrefix(source, "http") {
        resp, err := http.Get(source)
        if err != nil {
            return nil, err
        }
        defer resp.Body.Close()
        reader = resp.Body
    } else {
        file, err := os.Open(filepath.Clean(source))
        if err != nil {
            return nil, err
        }
        defer file.Close()
        reader = file
    }
    var data []JSONMODEL
    if err := json.NewDecoder(reader).Decode(&data); err != nil {
        return nil, err
    }
    return data, nil
}
📝 The
SOURCEconstant at the top of the code lets you easily switch between these modes:const SOURCE = "LOCAL_PATH" // 👈 Change this to your local file or remote URL
🌐 Routing & Dynamic Access
We use Go’s http.HandleFunc to create endpoints. Here’s how we handle dynamic routes like /api/0, which returns the first item in the JSON array:
func getNestedValue(data []JSONMODEL, parts []string) interface{} {
    if len(parts) == 0 {
        return data
    }
    index := parseInt(parts[0])
    if index >= 0 && index < len(data) {
        return data[index]
    }
    return nil
}
This function safely parses the route segment as an integer index and returns the corresponding object.
✏️ Update & Delete Operations
We support updating and deleting items by index. For example:
- PUT /api/1 updates the second item
 - DELETE /api/1 removes the second item
 
Here’s how we implement update logic:
func setNestedValue(data []JSONMODEL, parts []string, value interface{}) ([]JSONMODEL, error) {
    // ...
}
And deletion:
func deleteNestedValue(data []JSONMODEL, parts []string) ([]JSONMODEL, error) {
    // ...
}
These functions modify the global jsonData slice in memory.
⚙️ Middleware: Rate Limiting & CORS
To protect our server from abuse and make it accessible across domains, we implement two middleware functions.
🚦 Rate Limiting
Limits clients to 5 requests per second:
const (
    rateLimitRequests = 5
    rateLimitWindow   = time.Second
)
It uses a map to track IP addresses and their last request time:
var visits = make(map[string]time.Time)
If a client exceeds the limit, they receive a 429 Too Many Requests response.
🔒 CORS Support
Allows cross-origin requests from any domain:
w.Header().Set("Access-Control-Allow-Origin", "*")
Also handles preflight OPTIONS requests gracefully.
🖥️ Starting the Server
Our server runs on port 3000:
const port = "3000"
We wrap the server start in a goroutine so we can handle graceful shutdowns when receiving OS signals:
quit := make(chan os.Signal, 1)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
    log.Fatal("Server forced to shutdown:", err)
}
📦 Final Thoughts
This small Go server demonstrates the power of the standard library. With no external dependencies, we've built:
- A JSON loader
 - A RESTful API
 - Middleware for security and performance
 - Graceful shutdown handling
 
It's a great starting point for building mock services, internal tools, or even embedded servers in larger applications.
🧪 Try It Yourself
You can run this server locally with:
go run main.go
Then visit:
http://localhost:3000/api/0
Or send POST, PUT, or DELETE requests to manipulate the data.
Full Code
package main
import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net"
    "net/http"
    "os"
    "path/filepath"
    "strings"
    "sync"
    "time"
)
// 🔁 Hardcoded source: local file or remote URL
const SOURCE = "LOCAL_PATH" // 👈 Change this to your local file or remote URL
// 🧱 Your custom struct — replace this with what quickjson.io generates for your JSON
type JSONMODEL struct {
    Name     string `json:"name"`
    ID       int32  `json:"id"`
    PASSWORD string `json:"password"`
}
var jsonData []JSONMODEL // 👈 Adjust this type to match your data structure
// Rate limiting config
const (
    rateLimitRequests = 5
    rateLimitWindow   = time.Second
)
var (
    visits = make(map[string]time.Time)
    mu     sync.Mutex
)
// loadData loads JSON from local file or remote URL into typed struct
func loadData(source string) ([]JSONMODEL, error) {
    var reader io.Reader
    if strings.HasPrefix(source, "http") {
        resp, err := http.Get(source)
        if err != nil {
            return nil, err
        }
        defer resp.Body.Close()
        reader = resp.Body
    } else {
        file, err := os.Open(filepath.Clean(source))
        if err != nil {
            return nil, err
        }
        defer file.Close()
        reader = file
    }
    var data []JSONMODEL
    if err := json.NewDecoder(reader).Decode(&data); err != nil {
        return nil, err
    }
    return data, nil
}
// getNestedValue simulates dynamic access (for array index like /api/0)
func getNestedValue(data []JSONMODEL, parts []string) interface{} {
    if len(parts) == 0 {
        return data
    }
    index := parseInt(parts[0])
    if index >= 0 && index < len(data) {
        return data[index]
    }
    return nil
}
// setNestedValue allows updating an item by index
func setNestedValue(data []JSONMODEL, parts []string, value interface{}) ([]JSONMODEL, error) {
    if len(parts) == 0 {
        return data, fmt.Errorf("no index provided")
    }
    index := parseInt(parts[0])
    if index < 0 || index >= len(data) {
        return data, fmt.Errorf("index out of bounds")
    }
    if v, ok := value.(JSONMODEL); ok {
        data[index] = v
    } else {
        return data, fmt.Errorf("invalid type for update")
    }
    return data, nil
}
// deleteNestedValue removes an item by index
func deleteNestedValue(data []JSONMODEL, parts []string) ([]JSONMODEL, error) {
    if len(parts) == 0 {
        return data, fmt.Errorf("no index provided")
    }
    index := parseInt(parts[0])
    if index < 0 || index >= len(data) {
        return data, fmt.Errorf("index out of bounds")
    }
    return append(data[:index], data[index+1:]...), nil
}
// parseInt safely parses int from string
func parseInt(s string) int {
    n := 0
    for _, r := range s {
        n = n*10 + int(r-'0')
    }
    return n
}
// Standard lib-based rate limiter middleware
func rateLimit(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ip, _, _ := net.SplitHostPort(r.RemoteAddr)
        mu.Lock()
        defer mu.Unlock()
        lastSeen := visits[ip]
        now := time.Now()
        if !lastSeen.IsZero() && now.Sub(lastSeen) < rateLimitWindow {
            http.Error(w, `{"error": "rate limit exceeded"}`, http.StatusTooManyRequests)
            return
        }
        visits[ip] = now
        next(w, r)
    }
}
// CORS middleware
func enableCORS(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusOK)
            return
        }
        next(w, r)
    }
}
// handler for dynamic routes
func jsonHandler(data *[]JSONMODEL) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        path := strings.Trim(r.URL.Path, "/")
        parts := strings.Split(path, "/")
        switch r.Method {
        case http.MethodGet:
            value := getNestedValue(*data, parts)
            if value == nil {
                http.Error(w, `{"error": "not found"}`, http.StatusNotFound)
                return
            }
            json.NewEncoder(w).Encode(value)
        case http.MethodPost:
            fallthrough
        case http.MethodPut:
            var payload JSONMODEL
            if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
                http.Error(w, `{"error": "invalid request body"}`, http.StatusBadRequest)
                return
            }
            if len(parts) == 0 {
                http.Error(w, `{"error": "no index provided"}`, http.StatusBadRequest)
                return
            }
            updatedData, err := setNestedValue(*data, parts, payload)
            if err != nil {
                http.Error(w, `{"error": "update failed"}`, http.StatusInternalServerError)
                return
            }
            *data = updatedData
            json.NewEncoder(w).Encode(payload)
        case http.MethodDelete:
            if len(parts) == 0 {
                http.Error(w, `{"error": "nothing to delete"}`, http.StatusBadRequest)
                return
            }
            updatedData, err := deleteNestedValue(*data, parts)
            if err != nil {
                http.Error(w, `{"error": "delete failed"}`, http.StatusInternalServerError)
                return
            }
            *data = updatedData
            w.WriteHeader(http.StatusOK)
            json.NewEncoder(w).Encode(map[string]string{"status": "deleted"})
        default:
            http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
        }
    }
}
func main() {
    const port = "3000"
    server := &http.Server{
        Addr:         ":" + port,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  120 * time.Second,
    }
    // Load JSON at startup
    log.Printf("Loading JSON from: %s", SOURCE)
    tempData, err := loadData(SOURCE)
    if err != nil {
        log.Fatalf("Failed to load JSON: %v", err)
    }
    jsonData = tempData
    // Root route
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(jsonData)
    })
    // API endpoints
    http.HandleFunc("/api/", enableCORS(rateLimit(func(w http.ResponseWriter, r *http.Request) {
        jsonHandler(&jsonData)(w, r)
    })))
    log.Printf("✅ JSON Server running at http://localhost:%s", port)
    // Start server in goroutine to allow graceful shutdown
    go func() {
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Error starting server: %v", err)
        }
    }()
    // Wait for interrupt signal to gracefully shut down
    quit := make(chan os.Signal, 1)
    <-quit
    log.Println("Shutting down server...")
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err := server.Shutdown(ctx); err != nil {
        log.Fatal("Server forced to shutdown:", err)
    }
    log.Println("Server exited")
}
📚 Summary
| Feature | Description | 
|---|---|
| Language | Go (Golang) | 
| Framework | Standard Library (net/http) | 
| Load Source | Local file or Remote URL | 
| Supported Methods | GET, POST, PUT, DELETE | 
| Middleware | Rate Limiting, CORS | 
| Graceful Shutdown | Yes | 
| Dependencies | None | 
    
Top comments (0)