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/http
package - 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
SOURCE
constant 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)