Golang can build useful web APIs with only the standard library. The net/http package provides servers, handlers, requests, responses, headers, status codes, and routing basics.
A handler is a function that receives http.ResponseWriter and *http.Request. The request contains method, URL, headers, query parameters, and body. The response writer sends headers, status, and body data back to the client.
Many production teams use routers or frameworks, but learning the standard library first helps you understand what those tools are doing under the hood.
package main
import (
"encoding/json"
"log"
"net/http"
)
type Tutorial struct {
ID int `json:"id"`
Title string `json:"title"`
}
func tutorialsHandler(w http.ResponseWriter, r *http.Request) {
tutorials := []Tutorial{
{ID: 1, Title: "Golang Syntax"},
{ID: 2, Title: "Golang Web API"},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tutorials)
}
func main() {
http.HandleFunc("/tutorials", tutorialsHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
A handler should write exactly one response for each request path. Set headers first, then set the status code, then write the response body.
If you call json.NewEncoder(w).Encode(...) without calling WriteHeader, Golang sends 200 OK automatically when the body is written. For non-200 responses, call WriteHeader before writing the body.
| Step | Example |
|---|---|
| Set header | w.Header().Set("Content-Type", "application/json") |
| Set status | w.WriteHeader(http.StatusCreated) |
| Write body | json.NewEncoder(w).Encode(response) |
Real APIs should validate the HTTP method. A list endpoint usually allows GET, a create endpoint usually allows POST, and unsupported methods should return 405 Method Not Allowed.
Status codes are part of the API contract. Use them consistently so clients can handle success and failure without reading fragile text messages.
| Status | Common Meaning |
|---|---|
200 OK | Request succeeded and response body is returned. |
201 Created | A new resource was created. |
400 Bad Request | Input is invalid or malformed. |
404 Not Found | Requested resource does not exist. |
405 Method Not Allowed | Endpoint exists, but the method is unsupported. |
500 Internal Server Error | Unexpected server failure. |
func tutorialsHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode([]string{"Golang", "TypeScript"})
}
Use query parameters for optional filters, pagination, search terms, and sorting options. They are available through r.URL.Query().
Query values arrive as strings, so parse and validate them before use. Do not trust a query parameter just because it exists.
func listTutorialsHandler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
pageValue := query.Get("page")
page := 1
if pageValue != "" {
parsed, err := strconv.Atoi(pageValue)
if err != nil || parsed < 1 {
http.Error(w, "invalid page", http.StatusBadRequest)
return
}
page = parsed
}
json.NewEncoder(w).Encode(map[string]int{"page": page})
}
For endpoints that accept JSON, decode the request body into a struct. After decoding, validate required fields and business rules. Valid JSON syntax does not mean the request is valid for your application.
Use struct tags such as json:"title" to control field names. Keep request structs separate from database models when validation or public API shape differs from storage shape.
type CreateTutorialRequest struct {
Title string `json:"title"`
}
func createTutorialHandler(w http.ResponseWriter, r *http.Request) {
var input CreateTutorialRequest
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
if input.Title == "" {
http.Error(w, "title is required", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusCreated)
}
For APIs, JSON error responses are often easier for clients to consume than plain text. A small helper keeps error formatting consistent across handlers.
Keep public error messages safe. Do not expose database errors, stack traces, secrets, or internal infrastructure details to API clients.
type ErrorResponse struct {
Error string `json:"error"`
}
func writeJSONError(w http.ResponseWriter, status int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(ErrorResponse{Error: message})
}
A clean handler should focus on HTTP details: read input, validate simple request shape, call a service, and write a response. Business rules should live in functions or services that are easy to test without a web server.
This separation becomes important as the API grows. It keeps routing, JSON, validation, database calls, and business rules from turning into one giant handler function.
| Layer | Responsibility |
|---|---|
| Handler | HTTP method, request parsing, status codes, response format |
| Service | Business rules and use-case orchestration |
| Repository | Database or external storage access |
| Model | Data structures used by the app |
Middleware wraps a handler with shared behavior. Common middleware handles logging, panic recovery, authentication, request IDs, CORS, timeouts, and metrics.
In the standard library, middleware is usually a function that accepts an http.Handler and returns another http.Handler.
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/tutorials", tutorialsHandler)
log.Fatal(http.ListenAndServe(":8080", logging(mux)))
}
The standard library includes net/http/httptest, which lets you test handlers without opening a real network port. This makes API tests fast and predictable.
Test success cases, invalid methods, bad JSON, missing fields, query validation, and service errors. Good API tests verify both the status code and the response body.
net/http is enough to build practical Golang APIs.
http.ResponseWriter and *http.Request.
Explore 500+ free tutorials across 20+ languages and frameworks.