Modern backend development often starts with a framework. But sometimes the best way to understand a language is to remove the abstractions.
In this project, I built a minimal REST API in Go using only the standard library, without any external frameworks.
The goal is simple: demonstrate how HTTP APIs work internally in Go.
👉 Project repository: go-native-api-sample
Why Build an API Without Frameworks?
Frameworks are powerful, but they can hide important concepts.
When you build an API using only Go's native packages, you understand:
- how HTTP servers actually work
- how routing is handled
- how JSON serialization works
- how concurrency and shared state must be managed
Go’s standard library is already powerful enough for many services.
Project Structure
The project is intentionally simple.
go-native-api-sample
┣ main.go
â”— README.md
All the logic is inside main.go, making it easy to explore how everything works.
Starting the Server
The server is created using Go’s net/http package.
func main() {
http.HandleFunc("/health", healthHandler)
http.HandleFunc("/users", usersHandler)
log.Println("Server running on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
This registers two endpoints:
/health/users
And starts the HTTP server on port 8080.
Health Check Endpoint
A basic health endpoint is useful in production systems for monitoring and load balancers.
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
Test it with:
curl http://localhost:8080/health
Response:
OK
User Model
The API stores users in memory.
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
Since multiple requests can happen simultaneously, we also use a mutex to protect shared data.
var (
users []User
mu sync.Mutex
)
Creating Users (POST)
To create users, the API accepts JSON.
Example request:
{
"name": "Allan"
}
Handler example:
func createUser(w http.ResponseWriter, r *http.Request) {
var user User
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
mu.Lock()
user.ID = len(users) + 1
users = append(users, user)
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
Test with curl:
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"Allan"}'
Listing Users (GET)
To retrieve users:
func listUsers(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
Request:
curl http://localhost:8080/users
Response example:
[
{
"id": 1,
"name": "Allan"
}
]
Request Flow
Below is a simplified diagram of how the API works.
This shows the flow:
- Client sends HTTP request
- Go HTTP server receives it
- Router maps the endpoint
- Handler processes request
- Data is stored in memory safely using a mutex
Running the Project
Clone the repository:
git clone https://github.com/allanroberto18/go-native-api-sample
Run the API:
go run main.go
Server will start on: http://localhost:8080
Why This Project Matters
Small projects like this are incredibly valuable for learning.
Before introducing routers, ORMs, dependency injection, and frameworks, it’s important to understand:
- how a request enters your application
- how it's processed
- how responses are generated
Once you understand the fundamentals, frameworks become tools rather than magic.
Possible Improvements
This project could evolve into a more realistic API by adding:
- persistent storage (PostgreSQL)
- a router like Chi or Gin
- middleware (logging, auth)
- environment configuration
- Docker support
- structured logging
Final Thoughts
Go’s standard library is surprisingly capable.
You can build fully functional APIs without any framework, which is one of the reasons Go is so popular for backend services and microservices.
If you're learning Go, building something like this from scratch is one of the best exercises you can do.

Top comments (0)