├── .gitignore ├── go.mod ├── go.sum ├── README.md ├── bird_handlers.go ├── main.go ├── bird_handlers_test.go ├── assets └── index.html └── main_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .vscode 3 | debug* 4 | blog_example__go_web_app 5 | birdpedia -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sohamkamani/birdpedia 2 | 3 | go 1.14 4 | 5 | require github.com/gorilla/mux v1.8.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 2 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is the example repository for [this blog post](https://www.sohamkamani.com/golang/how-to-build-a-web-application/) 2 | 3 | To run the server on your system: 4 | 5 | 1. Run `go build` to create the binary (`birdpedia`) 6 | 1. Run the binary : `./birdpedia` 7 | 8 | To run tests, run `go test ./...` 9 | -------------------------------------------------------------------------------- /bird_handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | type Bird struct { 10 | Species string `json:"species"` 11 | Description string `json:"description"` 12 | } 13 | 14 | var birds []Bird 15 | 16 | func getBirdHandler(w http.ResponseWriter, r *http.Request) { 17 | //Convert the "birds" variable to json 18 | birdListBytes, err := json.Marshal(birds) 19 | 20 | // If there is an error, print it to the console, and return a server 21 | // error response to the user 22 | if err != nil { 23 | fmt.Println(fmt.Errorf("Error: %v", err)) 24 | w.WriteHeader(http.StatusInternalServerError) 25 | return 26 | } 27 | // If all goes well, write the JSON list of birds to the response 28 | w.Write(birdListBytes) 29 | } 30 | 31 | func createBirdHandler(w http.ResponseWriter, r *http.Request) { 32 | // Create a new instance of Bird 33 | bird := Bird{} 34 | 35 | // We send all our data as HTML form data 36 | // the `ParseForm` method of the request, parses the 37 | // form values 38 | err := r.ParseForm() 39 | 40 | // In case of any error, we respond with an error to the user 41 | if err != nil { 42 | fmt.Println(fmt.Errorf("Error: %v", err)) 43 | w.WriteHeader(http.StatusInternalServerError) 44 | return 45 | } 46 | 47 | // Get the information about the bird from the form info 48 | bird.Species = r.Form.Get("species") 49 | bird.Description = r.Form.Get("description") 50 | 51 | // Append our existing list of birds with a new entry 52 | birds = append(birds, bird) 53 | 54 | //Finally, we redirect the user to the original HTMl page (located at `/assets/`) 55 | http.Redirect(w, r, "/assets/", http.StatusFound) 56 | } 57 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | ) 9 | 10 | // The new router function creates the router and 11 | // returns it to us. We can now use this function 12 | // to instantiate and test the router outside of the main function 13 | func newRouter() *mux.Router { 14 | r := mux.NewRouter() 15 | r.HandleFunc("/hello", handler).Methods("GET") 16 | 17 | // Declare the static file directory and point it to the directory we just made 18 | staticFileDirectory := http.Dir("./assets/") 19 | // Declare the handler, that routes requests to their respective filename. 20 | // The fileserver is wrapped in the `stripPrefix` method, because we want to 21 | // remove the "/assets/" prefix when looking for files. 22 | // For example, if we type "/assets/index.html" in our browser, the file server 23 | // will look for only "index.html" inside the directory declared above. 24 | // If we did not strip the prefix, the file server would look for "./assets/assets/index.html", and yield an error 25 | staticFileHandler := http.StripPrefix("/assets/", http.FileServer(staticFileDirectory)) 26 | // The "PathPrefix" method acts as a matcher, and matches all routes starting 27 | // with "/assets/", instead of the absolute route itself 28 | r.PathPrefix("/assets/").Handler(staticFileHandler).Methods("GET") 29 | 30 | r.HandleFunc("/bird", getBirdHandler).Methods("GET") 31 | r.HandleFunc("/bird", createBirdHandler).Methods("POST") 32 | return r 33 | } 34 | 35 | func main() { 36 | // The router is now formed by calling the `newRouter` constructor function 37 | // that we defined above. The rest of the code stays the same 38 | r := newRouter() 39 | err := http.ListenAndServe(":8080", r) 40 | if err != nil { 41 | panic(err.Error()) 42 | } 43 | } 44 | 45 | func handler(w http.ResponseWriter, r *http.Request) { 46 | fmt.Fprintf(w, "Hello World!") 47 | } 48 | -------------------------------------------------------------------------------- /bird_handlers_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "net/url" 9 | "strconv" 10 | "testing" 11 | ) 12 | 13 | func TestGetBirdsHandler(t *testing.T) { 14 | 15 | birds = []Bird{ 16 | {"sparrow", "A small harmless bird"}, 17 | } 18 | 19 | req, err := http.NewRequest("GET", "", nil) 20 | 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | recorder := httptest.NewRecorder() 25 | 26 | hf := http.HandlerFunc(getBirdHandler) 27 | 28 | hf.ServeHTTP(recorder, req) 29 | 30 | if status := recorder.Code; status != http.StatusOK { 31 | t.Errorf("handler returned wrong status code: got %v want %v", 32 | status, http.StatusOK) 33 | } 34 | 35 | expected := Bird{"sparrow", "A small harmless bird"} 36 | b := []Bird{} 37 | err = json.NewDecoder(recorder.Body).Decode(&b) 38 | 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | actual := b[0] 44 | 45 | if actual != expected { 46 | t.Errorf("handler returned unexpected body: got %v want %v", actual, expected) 47 | } 48 | } 49 | func TestCreateBirdsHandler(t *testing.T) { 50 | 51 | birds = []Bird{ 52 | {"sparrow", "A small harmless bird"}, 53 | } 54 | 55 | form := newCreateBirdForm() 56 | req, err := http.NewRequest("POST", "", bytes.NewBufferString(form.Encode())) 57 | 58 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 59 | req.Header.Add("Content-Length", strconv.Itoa(len(form.Encode()))) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | recorder := httptest.NewRecorder() 64 | 65 | hf := http.HandlerFunc(createBirdHandler) 66 | 67 | hf.ServeHTTP(recorder, req) 68 | 69 | if status := recorder.Code; status != http.StatusFound { 70 | t.Errorf("handler returned wrong status code: got %v want %v", 71 | status, http.StatusOK) 72 | } 73 | 74 | expected := Bird{"eagle", "A bird of prey"} 75 | 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | actual := birds[1] 81 | 82 | if actual != expected { 83 | t.Errorf("handler returned unexpected body: got %v want %v", actual, expected) 84 | } 85 | } 86 | 87 | func newCreateBirdForm() *url.Values { 88 | form := url.Values{} 89 | form.Set("species", "eagle") 90 | form.Set("description", "A bird of prey") 91 | return &form 92 | } 93 | -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | The bird encyclopedia 9 | 10 | 11 | 12 |

The bird encyclopedia

13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
SpeciesDescription
PigeonCommon in cities
26 |
27 | 28 | 32 |
33 | Species: 34 | 35 |
Description: 36 | 37 |
38 | 39 |
40 | 41 | 46 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestHandler(t *testing.T) { 11 | //Here, we form a new HTTP request. This is the request that's going to be passed to our handler. 12 | // The first argument is the method, the second argument is the route, and the third is the request body, which we don't have in this case. 13 | req, err := http.NewRequest("GET", "", nil) 14 | 15 | // In case there is an error in forming the request, we fail and stop the test 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | // We use Go's httptest library to create an http recorder. This recorder will act as the target of our http request 21 | // (you can think of it as a mini-browser, which will accept the result of the http request that we make) 22 | recorder := httptest.NewRecorder() 23 | 24 | // Create an HTTP handler from our handler function. "handler" is the handler function defined in our main.go file that we want to test 25 | hf := http.HandlerFunc(handler) 26 | 27 | // Serve the HTTP request to our recorder. This is the line that actually executes our the handler that we want to test 28 | hf.ServeHTTP(recorder, req) 29 | 30 | // Check the status code is what we expect. 31 | if status := recorder.Code; status != http.StatusOK { 32 | t.Errorf("handler returned wrong status code: got %v want %v", 33 | status, http.StatusOK) 34 | } 35 | 36 | // Check the response body is what we expect. 37 | expected := `Hello World!` 38 | actual := recorder.Body.String() 39 | if actual != expected { 40 | t.Errorf("handler returned unexpected body: got %v want %v", actual, expected) 41 | } 42 | } 43 | 44 | func TestRouter(t *testing.T) { 45 | // Instantiate the router using the constructor function that 46 | // we defined previously 47 | r := newRouter() 48 | 49 | // Create a new server using the "httptest" libraries `NewServer` method 50 | // Documentation : https://golang.org/pkg/net/http/httptest/#NewServer 51 | mockServer := httptest.NewServer(r) 52 | 53 | // The mock server we created runs a server and exposes its location in the URL attribute 54 | // We make a GET request to the "hello" route we defined in the router 55 | resp, err := http.Get(mockServer.URL + "/hello") 56 | 57 | // Handle any unexpected error 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | 62 | // We want our status to be 200 (ok) 63 | if resp.StatusCode != http.StatusOK { 64 | t.Errorf("Status should be ok, got %d", resp.StatusCode) 65 | } 66 | 67 | // In the next few lines, the response body is read, and converted to a string 68 | defer resp.Body.Close() 69 | // read the body into a bunch of bytes (b) 70 | b, err := ioutil.ReadAll(resp.Body) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | // convert the bytes to a string 75 | respString := string(b) 76 | expected := "Hello World!" 77 | 78 | // We want our response to match the one defined in our handler. 79 | // If it does happen to be "Hello world!", then it confirms, that the 80 | // route is correct 81 | if respString != expected { 82 | t.Errorf("Response should be %s, got %s", expected, respString) 83 | } 84 | 85 | } 86 | 87 | func TestRouterForNonExistentRoute(t *testing.T) { 88 | r := newRouter() 89 | mockServer := httptest.NewServer(r) 90 | // Most of the code is similar. The only difference is that now we make a //request to a route we know we didn't define, like the `PUT /hello` route. 91 | resp, err := http.Post(mockServer.URL+"/hello", "", nil) 92 | 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | 97 | // We want our status to be 405 (method not allowed) 98 | if resp.StatusCode != http.StatusMethodNotAllowed { 99 | t.Errorf("Status should be 405, got %d", resp.StatusCode) 100 | } 101 | 102 | // The code to test the body is also mostly the same, except this time, we need an empty body 103 | defer resp.Body.Close() 104 | b, err := ioutil.ReadAll(resp.Body) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | respString := string(b) 109 | expected := "" 110 | 111 | if respString != expected { 112 | t.Errorf("Response should be %s, got %s", expected, respString) 113 | } 114 | 115 | } 116 | 117 | func TestStaticFileServer(t *testing.T) { 118 | r := newRouter() 119 | mockServer := httptest.NewServer(r) 120 | 121 | // We want to hit the `GET /assets/` route to get the index.html file response 122 | resp, err := http.Get(mockServer.URL + "/assets/") 123 | 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | 128 | // We want our status to be 200 (ok) 129 | if resp.StatusCode != http.StatusOK { 130 | t.Errorf("Status should be 200, got %d", resp.StatusCode) 131 | } 132 | 133 | // It isn't wise to test the entire content of the HTML file. 134 | // Instead, we test that the content-type header is "text/html; charset=utf-8" 135 | // so that we know that an html file has been served 136 | contentType := resp.Header.Get("Content-Type") 137 | expectedContentType := "text/html; charset=utf-8" 138 | 139 | if expectedContentType != contentType { 140 | t.Errorf("Wrong content type, expected %s, got %s", expectedContentType, contentType) 141 | } 142 | 143 | } 144 | --------------------------------------------------------------------------------