summaryrefslogtreecommitdiff
path: root/content/posts/2024-08-26-01.md
diff options
context:
space:
mode:
authoralex emery <[email protected]>2024-11-03 15:35:07 +0000
committeralex emery <[email protected]>2024-11-03 16:03:43 +0000
commitb28b124f1bc62a737bc4a11d575ff7638e65ee66 (patch)
tree19f78fa1ff45f158798e1922fb74fc8809d7b7a2 /content/posts/2024-08-26-01.md
parent508527f52de524a4fd174d386808e314b4138b11 (diff)
use obsidian symlink to posts
Diffstat (limited to 'content/posts/2024-08-26-01.md')
-rw-r--r--content/posts/2024-08-26-01.md252
1 files changed, 0 insertions, 252 deletions
diff --git a/content/posts/2024-08-26-01.md b/content/posts/2024-08-26-01.md
deleted file mode 100644
index dc25d77..0000000
--- a/content/posts/2024-08-26-01.md
+++ /dev/null
@@ -1,252 +0,0 @@
----
-title: "Writing HTTP Handlers"
-tags: posts
-toc: true
----
-
-I'm sharing how I write handlers in Go.
-
-I write them like this for reasons that are probably fairly contextual. I've written a few applications and had to swap REST libraries or even swapped REST for GRPC, so things that make that easier speak to me a great deal.
-
-I've used `ints` instead of the `http.StatusXXXX` and omitted `JSON` tags in an attempt to try save up screen space.
-
-To begin with, you might have something like this:
-```go
-package main
-
-import (
- "fmt"
- "log"
- "net/http"
-)
-
-func handler(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintf(w, "Hello, World!")
-}
-
-func main() {
- http.HandleFunc("/", handler)
- log.Fatal(http.ListenAndServe(":8080", nil))
-}
-
-```
-
-Then you might get told off because you've just registered routes with the default mux, which isn't very testable.
-
-So you tweak it a little bit.
-```go
-package main
-
-import (
- "fmt"
- "log"
- "net/http"
-)
-
-func handler(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintf(w, "Hello, World!")
-}
-
-func newMux() *http.ServeMux {
- mux := http.NewServeMux()
- mux.HandleFunc("/", handler)
-
- return mux
-}
-
-func Run() error {
- mux := newMux()
- return http.ListenAndServe(":8080", mux)
-}
-
-func main() {
- if err := Run(); err != nil {
- log.Fatal(err)
- }
-}
-```
-
-`newMux()` gives you a `mux` to use when testing.
-
-`Run` keeps `main` nice and clean, so you can just return errors as needed instead of going `log.Fatal` and just generally being messy.
-
-But now you need to do something real, you want to store and fetch data.
-
-```go
-package main
-
-import (
- "encoding/json"
- "fmt"
- "log"
- "net/http"
- "strconv"
-)
-
-func NewMux() *http.ServeMux {
- mux := http.NewServeMux()
- s := Server{
- data: make(map[int]Content),
- }
-
- s.Register(mux)
- return mux
-}
-
-func Run() error {
- mux := NewMux()
- return http.ListenAndServe(":8080", mux)
-}
-
-type Server struct {
- data map[int]Content
-}
-
-func (s *Server) Register(mux *http.ServeMux) {
- mux.HandleFunc("GET /{id}", s.Get)
- mux.HandleFunc("POST /", s.Post)
-}
-
-func (s *Server) Get(w http.ResponseWriter, r *http.Request) {
- idStr := r.PathValue("id")
- id, err := strconv.Atoi(idStr)
- if err != nil {
- w.WriteHeader(400)
- w.Write([]byte(fmt.Sprintf("failed to parse id: %v", err)))
- return
- }
- data, ok := s.data[id]
- if !ok {
- w.WriteHeader(404)
- w.Write([]byte("not found"))
- return
- }
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(200)
- json.NewEncoder(w).Encode(data)
-}
-
-type ContentPostReq struct {
- Foo string
-}
-
-func (s *Server) Post(w http.ResponseWriter, r *http.Request) {
- req := ContentPostReq{}
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- w.WriteHeader(400)
- w.Write([]byte(fmt.Sprintf("failed to parse request: %v", err)))
- return
- }
- id := len(s.data)
- content := Content{
- ID: id,
- Foo: req.Foo,
- }
- s.data[id] = content
-
- w.WriteHeader(200)
- json.NewEncoder(w).Encode(content)
-}
-
-type Content struct {
- ID int
- Foo string
-}
-
-func main() {
- if err := Run(); err != nil {
- log.Fatal(err)
- }
-}
-```
-
-```shell
-❯ curl -X POST localhost:8080 --header "Content-Type: application/json" -d '{"foo":"bar"}'
-{"ID":0,"Foo":"bar"}
-❯ curl -X GET localhost:8080/0
-{"ID":0,"Foo":"bar"}
-```
-
-Erm, well, okay. Quite a bit has changed here, but I'm sure you can read it. We now save and fetch very, very safely from a map and return the response as `JSON`. I've done some things for brevity because I want to get to the main point.
-
-This API is inconsistent. It sometimes returns `JSON`, and the others return strings. Overall, it's just a mess.
-
-So let's try to standardise things.
-First, let's design some form of REST spec.
-
-```go
-type JSONResp[T any] struct {
- Resources []T
- Errs []ErrorResp
-}
-
-type ErrorResp struct {
- Status int
- Msg string
-}
-```
-We want to be able to support fetching multiple resources at once, if we can only fetch some resources, let's return them under `resources` and show the errors under `errs`
-
-Now, add some helpful functions to handle things.
-```go
-func Post[In any, Out any](successCode int, fn func(context.Context, In) ([]Out, []ErrorResp)) func(http.ResponseWriter, *http.Request) {
- return func(w http.ResponseWriter, r *http.Request) {
- var v In
-
- if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
- writeJSONResp[Out](w, http.StatusBadRequest, nil, []ErrorResp{
- {
- Status: http.StatusBadRequest,
- Msg: fmt.Sprintf("failed to parse request: %v", err),
- },
- })
-
- return
- }
-
- res, errs := fn(r.Context(), v)
- writeJSONResp(w, successCode, res, errs)
- }
-}
-
-func writeJSONResp[T any](w http.ResponseWriter, successCode int, res []T, errs []ErrorResp) {
- body := JSONResp[T]{
- Resources: res,
- Errs: errs,
- }
-
- status := successCode
- for _, e := range errs {
- if e.Status > status {
- status = e.Status
- }
- }
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(status)
- json.NewEncoder(w).Encode(body)
-}
-```
-And we've standardised all `POST` requests!
-
-This function can be used by all `POST` requests, ensuring they adhere to the spec. It also removes the repetitive code around marshalling and unmarshalling to `JSON` and handles errors in a consistent manner.
-The handler functions accept a `context` param and their expected struct input.
-```go
-func (s *Server) Register(mux *http.ServeMux) {
-...
- mux.HandleFunc("POST /", Post(201, s.Post))
-}
-
-func (s *Server) Post(ctx context.Context, req ContentPostReq) ([]Content, []ErrorResp) {
- id := len(s.data)
- content := Content{
- ID: id,
- Foo: req.Foo,
- }
- s.data[id] = content
-
- return []Content{content}, nil
-}
-```
-As you can see, the post function is fairly cleaner now.
-
-You can extend this to all the other request types. If you have query or path parameters, you could either pass in the request, write a custom struct tag parser, or find someone else who has already done it: [https://github.com/gorilla/schema](https://github.com/gorilla/schema).