In Chapter 1, we built a server that responded with "hello world" to every request. That's like having an API that returns the same response whether your frontend calls /api/users
, /api/products
, or /api/login
. Not very useful!
Today, we'll fix this by making our server route-aware - different paths will return different responses, just like a real API.
What We've Built So Far
From Chapter 1, we have a basic server that:
- ✅ Listens on port 3000
- ✅ Responds to HTTP requests
- ❌ Returns the same response for every path
The Problem Frontend Developers Face
When you write JavaScript like this:
// Different endpoints, different data expected
const users = await fetch('/api/users').then(r => r.json())
const products = await fetch('/api/products').then(r => r.json())
const profile = await fetch('/api/profile').then(r => r.json())
You expect different responses from each endpoint. Our current server can't handle this - it would return "hello world" for all three calls.
Inspecting the Request Path
The solution starts with examining what the client is actually requesting. Let's modify our ServeHTTP
method to look at the request path:
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
log.Printf("%s %s", r.Method, path)
// For now, let's just echo back what was requested
message := fmt.Sprintf("You requested: %s", path)
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}
Let's test this incremental change:
go run .
In another terminal, try different paths:
curl http://localhost:3000/users
curl http://localhost:3000/products
curl http://localhost:3000/api/login
Expected Responses:
You requested: /users
You requested: /products
You requested: /api/login
Great! Now our server knows what path was requested. But we still need to return different content for different paths.
Building Route Logic with Switch Statements
Here's where most tutorials jump straight to a routing library. But let's first understand why those libraries exist by building routing logic ourselves.
The most straightforward approach is a switch statement:
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
log.Printf("%s %s", r.Method, path)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
switch path {
case "/hello":
hello(w)
case "/goodbye":
goodbye(w)
case "/time":
getTime(w)
default:
welcome(w)
}
}
Now we need to create those handler functions. Let's add them to our server.go
:
func welcome(w http.ResponseWriter) {
w.Write([]byte("Welcome to our Go Webserver!"))
}
func hello(w http.ResponseWriter) {
w.Write([]byte("Hello, World!"))
}
func goodbye(w http.ResponseWriter) {
w.Write([]byte("Goodbye, see you later!"))
}
func getTime(w http.ResponseWriter) {
message := fmt.Sprintf("Current Time: %s", time.Now().Format("2006-01-02 15:04:05"))
w.Write([]byte(message))
}
Don't forget to import the time
package at the top of your file:
import (
"fmt"
"log"
"net/http"
"time" // Add this line
)
Testing Our Route-Aware Server
Let's test our newly route-aware server:
go run .
Try these different endpoints:
curl http://localhost:3000/hello
curl http://localhost:3000/goodbye
curl http://localhost:3000/time
curl http://localhost:3000/
curl http://localhost:3000/unknown-path
Expected Responses:
Hello, World!
Goodbye, see you later!
Current Time: 2024-09-14 15:30:45
Welcome to our Go Webserver!
Welcome to our Go Webserver!
Perfect! Our server now behaves like a real API - different paths return different responses.
What We've Accomplished
We now have:
- ✅ Path-aware routing (different responses for different URLs)
- ✅ Clean handler separation (each route has its own function)
- ✅ Default route handling (catch-all for unknown paths)
- ✅ Request logging (see what's being requested)
The Problems We're Creating
While our switch statement approach works, it's already showing some issues that will become painful as we scale:
1. Method Blindness
Our server doesn't distinguish between HTTP methods. Try this:
curl -X POST http://localhost:3000/hello
curl -X DELETE http://localhost:3000/hello
curl -X PUT http://localhost:3000/hello
All return the same response! In a real API, GET /users
and POST /users
should do very different things.
2. Growing Switch Statement
Imagine adding 20 more routes:
switch path {
case "/hello":
hello(w)
case "/goodbye":
goodbye(w)
case "/time":
getTime(w)
case "/users":
getUsers(w)
case "/products":
getProducts(w)
case "/orders":
getOrders(w)
// ... 15 more cases
default:
welcome(w)
}
This becomes unwieldy quickly - like having one massive if-else chain in your frontend code.
3. No Dynamic Paths
What if your frontend needs to call /users/123
or /products/456
? We'd need a case for every possible ID, which is impossible.
4. Handler Signatures
Our handler functions only take a ResponseWriter
. Real APIs need access to the full Request
to read query parameters, headers, and request bodies.
Quick Fix: Adding HTTP Method Support
Before we move on, let's quickly add basic HTTP method support to see how the complexity grows:
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
method := r.Method
log.Printf("%s %s", method, path)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
// Now we need to check BOTH path AND method
switch method {
case "GET":
s.handleGET(w, r, path)
case "POST":
s.handlePOST(w, r, path)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *Server) handleGET(w http.ResponseWriter, r *http.Request, path string) {
switch path {
case "/hello":
hello(w)
case "/goodbye":
goodbye(w)
case "/time":
getTime(w)
default:
welcome(w)
}
}
func (s *Server) handlePOST(w http.ResponseWriter, r *http.Request, path string) {
switch path {
case "/hello":
w.Write([]byte("Hello via POST!"))
default:
http.Error(w, "Not found", http.StatusNotFound)
}
}
Test the different methods:
curl -X GET http://localhost:3000/hello # Returns: Hello, World!
curl -X POST http://localhost:3000/hello # Returns: Hello via POST!
curl -X PUT http://localhost:3000/hello # Returns: Method not allowed
See how quickly this is getting complex? We're already at nested switch statements, and we only have a few routes!
What's Next?
In Chapter 3, we'll solve these growing pains by building a proper router - a separate component responsible for matching paths and methods to handlers. This will give us:
- Clean separation of concerns (routing logic separate from business logic)
- Support for different HTTP methods
- A foundation for dynamic routes like
/users/:id
- Better organization as our API grows
Think of it like moving from one giant JavaScript file to a properly structured application with separate modules.
Challenge: Before Chapter 3, try adding a few more routes to see how unwieldy the switch statement becomes. Add routes like /api/users
, /api/products
, and /api/orders
for both GET and POST methods.
Top comments (0)