Create a REST API with Go
27 Sep 2017Let’s create a REST api using golang. In our example, we’ll walk through what’s required to make an API for a Todo-style application.
Starting off
First up, we’re going to create a project. I’ve called mine “todo”.
mkdir -p $GOPATH/src/github.com/tuttlem/todo
This gives us a project folder. Start off editing your main.go
file. We’ll pop the whole application into this single file, as it’ll be simple enough.
package main
import (
"fmt"
)
func main() {
fmt.Println("Todo application")
}
The Server
We can turn our console application now into a server application pretty easily with the net/http
module. Once we import this, we’ll use the ListenAndServe
function to stand a server up. While we’re at it, we’ll create a NotImplementedHandler
so we can assertivly tell our calling clients that we haven’t done anything just yet.
package main
import (
"net/http"
)
func main() {
// start the server listening, and always sending back
// the "NotImplemented" message
http.ListenAndServe(":3000", NotImplementedHandler);
}
var NotImplementedHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotImplemented)
})
Testing this service will be a little pointless, but we can see our 501’s being thrown:
➜ ~ curl --verbose http://localhost:3000/something
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /something HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 501 Not Implemented
< Content-Type: application/json
< Date: Wed, 27 Sep 2017 13:26:33 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact
Routing
Routing will allow us to direct a user’s request to the correct piece of functionality. Routing also helps us extract input parameters for requests. Using mux from gorilla we can quickly setup the list, create, update and delete endpoints we need to accomplish our TODO application.
import (
// . . .
"github.com/gorilla/mux"
// . . .
)
func main() {
r := mux.NewRouter()
r.Handle("/todos", NotImplementedHandler).Methods("GET")
r.Handle("/todos", NotImplementedHandler).Methods("POST")
r.Handle("/todos/{id}", NotImplementedHandler).Methods("PUT")
r.Handle("/todos/{id}", NotImplementedHandler).Methods("DELETE")
// start the server listening, and always sending back
// the "NotImplemented" message
http.ListenAndServe(":3000", r);
}
What’s nice about this, is that our actual routes are what will emit the 501
. Anything that completely misses the router will result in a much more accurate 404
. Perfect.
Handlers
We can give the server some handlers now. A handler takes the common shape of:
func handler(w http.ResponseWriter, r *http.Request) {
}
The http.ResponseWriter
typed w
parameter is what we’ll use to send a payload back to the client. r
takes the form of the request, and it’s what we’ll use as an input to the process. This is all looking very “server’s output as a function of its input” to me.
var ListTodoHandler = NotImplementedHandler
var CreateTodoHandler = NotImplementedHandler
var UpdateTodoHandler = NotImplementedHandler
var DeleteTodoHandler = NotImplementedHandler
Which means that our router (whilst still unimplemented) starts to make a little more sense.
r.Handle("/todos", ListTodoHandler).Methods("GET")
r.Handle("/todos", CreateTodoHandler).Methods("POST")
r.Handle("/todos/{id}", UpdateTodoHandler).Methods("PUT")
r.Handle("/todos/{id}", DeleteTodoHandler).Methods("DELETE")
Modelling data
We need to start modelling this data so that we can prepare an API to work with it. The following type
declaration creates a structure that will define our todo item:
type Todo struct {
Id int `json:"id"`
Description string `json:"description"`
Complete bool `json:"complete"`
}
Note the json
directives at the end of each of the members in the structure. This is allowing us to control how the member is represented as an encoded JSON value. A more idomatic JSON has lowercased member names.
The “database” that our API will manage is a slice.
var Todos []Todo
var Id int
// . . . inside "main"
// Initialize the todo "database"
Id = 1
Todos = []Todo{ Todo{Id: Id, Description: "Buy Cola"} }
Implementation
To “list” out todo items, we simply return the encoded slice.
var ListTodoHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(Todos);
})
Creating an item is a bit more complex due to value marshalling.
var CreateTodoHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body);
var newTodo Todo
err := decoder.Decode(&newTodo)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
defer r.Body.Close()
Id ++
newTodo.Id = Id
Todos = append(Todos, newTodo)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Id);
})
In order to implement a delete function, we need a Filter
implementation that knows about Todo
objects.
func Filter(vs []Todo, f func(Todo) bool) []Todo {
vsf := make([]Todo, 0)
for _, v := range vs {
if f(v) {
vsf = append(vsf, v)
}
}
return vsf
}
We then add a reference to strconv
because we’ll need Atoi
to take in the string
id
and convert it to an int
. Remember, the Id
attribute of our Todo
object is an int
.
var DeleteTodoHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id, _ := strconv.Atoi(params["id"])
Todos = Filter(Todos, func(t Todo) bool {
return t.Id != id
})
w.WriteHeader(http.StatusNoContent)
})
Finally, an update. We’ll do the same thing as a DELETE
, but we’ll swap the posted object in.
var UpdateTodoHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id, _ := strconv.Atoi(params["id"])
Todos = Filter(Todos, func(t Todo) bool {
return t.Id != id
})
decoder := json.NewDecoder(r.Body);
var newTodo Todo
err := decoder.Decode(&newTodo)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
defer r.Body.Close()
newTodo.Id = id
Todos = append(Todos, newTodo)
w.WriteHeader(http.StatusNoContent)
})
The UpdateTodoHandler
appears to be a mix of the delete action as well as create.
Up and running
You’re just about done. The Todo api is doing what we’ve asked it to do. The only thing left now, is to get some logging going. We’ll do that with some clever middleware again, from gorilla that will do just that.
import (
// . . .
"os"
"github.com/gorilla/handlers"
// . . .
)
// . . down in main() now
http.ListenAndServe(":3000",
handlers.LoggingHandler(os.Stdout, r))
This now gives us a status on requests hitting our server.
That’s all
That’s all for now. The full source is available as a gist.