Tutorials Logic, IN info@tutorialslogic.com

golang web api: Build a REST Endpoint with net/http

Building a Small API

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.

Basic JSON API

Basic JSON API
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))
}

Handlers and ResponseWriter

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)

HTTP Methods and Status Codes

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.

Method Guard

Method Guard
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"})
}

Query Parameters

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.

Read Query Parameters

Read Query Parameters
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})
}

Reading JSON Request Bodies

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.

Decode and Validate JSON

Decode and Validate JSON
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)
}

Writing JSON Errors

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.

JSON Error Helper

JSON Error Helper
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})
}

Separating Handler and Business Logic

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 Basics

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.

Logging Middleware

Logging Middleware
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)))
}

Testing API Handlers

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.

  • Return JSON with the correct Content-Type header.
  • Use clear status codes such as 200, 201, 400, 404, 405, and 500.
  • Validate path values, query parameters, and request bodies.
  • Keep handlers small enough to test comfortably.
  • Use httptest.NewRecorder and httptest.NewRequest for handler tests.
Before you move on

golang web api: Build a REST Endpoint with net/http Mastery Check

7 checks
  • net/http is enough to build practical Golang APIs.
  • Handlers receive http.ResponseWriter and *http.Request.
  • Set headers and status codes before writing the body.
  • Validate request method, query parameters, JSON body, and required fields.
  • Use JSON helpers for consistent API responses.
  • Move business logic out of handlers so it can be tested directly.
  • Middleware wraps handlers with shared cross-cutting behavior.
Next Step
Next Practice

Finish the concept here, then reinforce it with hands-on coding, interview prep, or a tool that matches the topic.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.