One job per handler
An HTTP handler should translate web input into your Go types, call your service logic, then translate the result back to HTTP.
Keep business rules out of the handler. This makes your API easier to test and easier for a recruiter to read.
Use net/http
Go ships with an HTTP server in the standard library. You can register a route with http.HandleFunc and start the server with http.ListenAndServe.
:8080 means your service listens on port 8080 on any interface. ListenAndServe blocks, so your program keeps running until you stop it.
Shape the endpoint
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done
Minimal API
This example stores tasks in memory. It shows typed handlers, maps, JSON, and method checks. Use this pattern with the types you created earlier in the track.
For Go 1.27, encoding/json keeps the familiar API while using the newer implementation behind it, so this code stays simple.
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
)
type Task struct {
ID int `json:"id"`
Text string `json:"text"`
Done bool `json:"done"`
}
type CreateTaskRequest struct {
Text string `json:"text"`
}
type ErrorResponse struct {
Error string `json:"error"`
}
var tasks = map[int]Task{}
var nextID = 1
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, ErrorResponse{Error: msg})
}
func tasksHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
list := make([]Task, 0, len(tasks))
for _, task := range tasks {
list = append(list, task)
}
writeJSON(w, http.StatusOK, list)
case http.MethodPost:
var req CreateTaskRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
req.Text = strings.TrimSpace(req.Text)
if req.Text == "" {
writeError(w, http.StatusBadRequest, "text is required")
return
}
task := Task{ID: nextID, Text: req.Text}
tasks[nextID] = task
nextID++
writeJSON(w, http.StatusCreated, task)
default:
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func taskByIDHandler(w http.ResponseWriter, r *http.Request) {
idText := strings.TrimPrefix(r.URL.Path, "/tasks/")
id, err := strconv.Atoi(idText)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid task id")
return
}
task, ok := tasks[id]
if !ok {
writeError(w, http.StatusNotFound, "task not found")
return
}
writeJSON(w, http.StatusOK, task)
}
func main() {
http.HandleFunc("/tasks", tasksHandler)
http.HandleFunc("/tasks/", taskByIDHandler)
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}Test with curl
Keep sample curl commands in your README. They prove your service runs without needing Postman or a frontend.
Run the server in one terminal. Use curl from another terminal.
go run .
curl http://localhost:8080/tasks
curl -X POST http://localhost:8080/tasks \
-H "Content-Type: application/json" \
-d '{"text":"pay mess bill by UPI"}'
curl http://localhost:8080/tasks/1Do not hide errors
A common beginner mistake is returning plain text errors sometimes and JSON errors other times. Pick one style for your API.
Also, do not ignore json.NewDecoder errors. Bad input should get a 400 response, not a fake empty task.
Do this now
Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.
0 of 5 done