Tutorials Logic, IN info@tutorialslogic.com
Navigation
Home About Us Contact Us Blogs FAQs
Tutorials
All Tutorials
Services
Academic Projects Resume Writing Website Development
Practice
Quiz Challenge Interview Questions Certification Practice
Tools
Online Compiler JSON Formatter Regex Tester CSS Unit Converter Color Picker
Compiler Tools

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
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.

StepExample
Set headerw.Header().Set("Content-Type", "application/json")
Set statusw.WriteHeader(http.StatusCreated)
Write bodyjson.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.

StatusCommon Meaning
200 OKRequest succeeded and response body is returned.
201 CreatedA new resource was created.
400 Bad RequestInput is invalid or malformed.
404 Not FoundRequested resource does not exist.
405 Method Not AllowedEndpoint exists, but the method is unsupported.
500 Internal Server ErrorUnexpected server failure.
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
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
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
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.

LayerResponsibility
HandlerHTTP method, request parsing, status codes, response format
ServiceBusiness rules and use-case orchestration
RepositoryDatabase or external storage access
ModelData 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
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.
Key Takeaways
  • 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.

Ready to Level Up Your Skills?

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