RESTful API is widely used in web project development. This article explains how Go language can implement RESTful JSON API step by step, and will also involve topics in RESTful design.
Maybe we have used all kinds of APIs before, and when we encounter APIs with bad designs, we feel extremely crashing. I hope that after this article, I can have a preliminary understanding of a well-designed RESTful API.
What is the JSON API?
Before JSON, many websites exchanged data through XML. If you contact JSON after using XML, you will undoubtedly feel how beautiful the world is. I won't go into the introduction of the JSON API here. If you are interested, please refer to it.jsonapi。
Basic Web Server
Fundamentally, RESTful services are first and foremost web services. Therefore, we can first take a look at how the basic web server is implemented in Go. The following example implements a simple web server, and for any request, the server responds to the requested URL back.
package main import ( "fmt" "html" "log" "net/http" ) func main() { ("/", func(w , r *) { (w, "Hello, %q", ()) }) ((":8080", nil)) }
The above basic web server uses two basic functions HandleFunc and ListenAndServe of the Go standard library.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { (pattern, handler) } func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return () }
Run the above basic web service, you can access http://localhost:8080 directly through the browser.
> go run basic_server.go
Add a route
Although the standard library contains routers, I find many people are confused about how it works. I have used various different third-party router libraries in my project. The most noteworthy one is the mux router of Gorilla Web ToolKit.
Another popular router is a package called httprouter from Julien Schmidt.
package main import ( "fmt" "html" "log" "net/http" "/gorilla/mux" ) func main() { router := ().StrictSlash(true) ("/", Index) ((":8080", router)) } func Index(w , r *) { (w, "Hello, %q", ()) }
To run the above code, first use go get to get the source code of the mux router:
> go get /gorilla/mux
The above code creates a basic router, assigns the Index processor to the request "/". When the client requests http://localhost:8080/, the Index processor will be executed.
If you are careful enough, you will find that the previous basic web service access http://localhost:8080/abc can respond normally: 'Hello, "/abc"', but after adding the route, you can only access http://localhost:8080. The reason is very simple, because we only added the resolution of "/", and the other routes are invalid routes, so they are all 404.
Create some basic routes
Since we have joined the route, we can add more routes in.
Suppose we want to create a basic ToDo application, so our code becomes like this:
package main import ( "fmt" "log" "net/http" "/gorilla/mux" ) func main() { router := ().StrictSlash(true) ("/", Index) ("/todos", TodoIndex) ("/todos/{todoId}", TodoShow) ((":8080", router)) } func Index(w , r *) { (w, "Welcome!") } func TodoIndex(w , r *) { (w, "Todo Index!") } func TodoShow(w , r *) { vars := (r) todoId := vars["todoId"] (w, "Todo Show:", todoId) }
Here we add two other routes: todos and todos/{todoId}.
This is the beginning of the RESTful API design.
Please note that for the last route we added a variable called todoId to the route after the route.
This allows us to pass ids to the route and can use specific records to respond to requests.
Basic Model
The routing is now ready, and it's time to create a Model, which can use the Model to send and retrieve data. In Go language, models can be implemented using structures, while in other languages, models are generally implemented using classes.
package main import ( "time" ) type Todo struct { Name string Completed bool Due } type Todos []Todo
Above we define a Todo structure to represent the items to be done. In addition, we also define a type Todos, which represents a list to be made, an array, or a shard.
You will see this later that it will become very useful.
Return some JSON
We have the basic model, so we can simulate some real responses. We can simulate some static data list for TodoIndex.
package main import ( "encoding/json" "fmt" "log" "net/http" "/gorilla/mux" ) // ... func TodoIndex(w , r *) { todos := Todos{ Todo{Name: "Write presentation"}, Todo{Name: "Host meetup"}, } (w).Encode(todos) } // ...
Now we create a static Todos shard to respond to client requests. Note that if you request http://localhost:8080/todos, you will get the following response:
[ { "Name": "Write presentation", "Completed": false, "Due": "0001-01-01T00:00:00Z" }, { "Name": "Host meetup", "Completed": false, "Due": "0001-01-01T00:00:00Z" } ]
Better Model
For experienced veterans, you may have found a problem. Each key in response to JSON is written in the initial letter. Although it seems trivial, capitalizing the initial letter of the key in response to JSON is not a habitual practice. Then, let’s teach you how to solve this problem:
type Todo struct { Name string `json:"name"` Completed bool `json:"completed"` Due `json:"due"` }
In fact, it is very simple, it is to add label attributes to the structure, so that you can completely control how the structure is orchestrated into JSON.
Split code
So far, all our code is in one file. It looks messy, it's time to split the code. We can split the code into the following multiple files according to the function.
We are going to create the following file and then move the corresponding code to the specific code file:
- : Program entry file.
- : Routing-related processor.
- : Routing.
- : todo related code.
package main import ( "encoding/json" "fmt" "net/http" "/gorilla/mux" ) func Index(w , r *) { (w, "Welcome!") } func TodoIndex(w , r *) { todos := Todos{ Todo{Name: "Write presentation"}, Todo{Name: "Host meetup"}, } if err := (w).Encode(todos); err != nil { panic(err) } } func TodoShow(w , r *) { vars := (r) todoId := vars["todoId"] (w, "Todo show:", todoId) }
package main import ( "net/http" "/gorilla/mux" ) type Route struct { Name string Method string Pattern string HandlerFunc } type Routes []Route func NewRouter() * { router := ().StrictSlash(true) for _, route := range routes { router. Methods(). Path(). Name(). Handler() } return router } var routes = Routes{ Route{ "Index", "GET", "/", Index, }, Route{ "TodoIndex", "GET", "/todos", TodoIndex, }, Route{ "TodoShow", "GET", "/todos/{todoId}", TodoShow, }, }
package main import "time" type Todo struct { Name string `json:"name"` Completed bool `json:"completed"` Due `json:"due"` } type Todos []Todo
package main import ( "log" "net/http" ) func main() { router := NewRouter() ((":8080", router)) }
Better Routing
During our reconstruction process, we created a routes file with more functions. This new file utilizes a structure containing multiple routing information. Note that here we can specify the type of request, such as GET, POST, DELETE, etc.
Output Web log
In the split routing file, I also include an ulterior motive. You will see later that it is easy to use other functions to modify the http processor after splitting.
First of all, we need to have the ability to log web requests, just like many popular web servers. In Go language, there is no web log package or function in the standard library, so we need to create it ourselves.
package logger import ( "log" "net/http" "time" ) func Logger(inner , name string) { return (func(w , r *) { start := () (w, r) ( "%s\t%s\t%s\t%s", , , name, (start), ) }) }
Above we defined a Logger function that can wrap and modify the handler.
This is a very standard idiomatic way in Go. In fact, it is also an idiomatic way of functional programming. Very efficient, we just need to pass the Handler into the function, and then it will wrap the incoming handler, add web logs and time-consuming statistics.
Apply Logger Modifier
To apply the Logger modifier, we can create a router. We just need to simply package all our current routes into it. The NewRouter function is modified as follows:
func NewRouter() * { router := ().StrictSlash(true) for _, route := range routes { var handler handler = handler = Logger(handler, ) router. Methods(). Path(). Name(). Handler(handler) } return router }
Now run our program again and we can see the logs as follows:
2014/11/19 12:41:39 GET /todos TodoIndex 148.324us
This routing file is crazy... Let's refactor it
The routes file has become slightly larger now. Let's break it down into multiple files:
package main import "net/http" type Route struct { Name string Method string Pattern string HandlerFunc } type Routes []Route var routes = Routes{ Route{ "Index", "GET", "/", Index, }, Route{ "TodoIndex", "GET", "/todos", TodoIndex, }, Route{ "TodoShow", "GET", "/todos/{todoId}", TodoShow, }, }
package main import ( "net/http" "/gorilla/mux" ) func NewRouter() * { router := ().StrictSlash(true) for _, route := range routes { var handler handler = handler = Logger(handler, ) router. Methods(). Path(). Name(). Handler(handler) } return router }
Also take some responsibility
So far we have some pretty good boilerplate code, and it's time to revisit our processor. We need a little more responsibility. First modify TodoIndex and add the following two lines of code:
func TodoIndex(w , r *) { todos := Todos{ Todo{Name: "Write presentation"}, Todo{Name: "Host meetup"}, } ().Set("Content-Type", "application/json; charset=UTF-8") () if err := (w).Encode(todos); err != nil { panic(err) } }
Two things happened here. First, we set the response type and tell the client to expect to accept JSON. Second, we have clearly set the response status code.
The net/http server in Go will try to guess the output content type for us (but not every time is accurate), but since we already know exactly the response type, we should always set it ourselves.
Wait for a moment, where is our database?
Obviously, if we were to create a RESTful API, we needed some place to store and retrieve data. However, this is not within the scope of this article, so we will simply create a very simple simulated database (non-thread-safe).
We create a file with the following content:
package main import "fmt" var currentId int var todos Todos // Give us some seed data func init() { RepoCreateTodo(Todo{Name: "Write presentation"}) RepoCreateTodo(Todo{Name: "Host meetup"}) } func RepoFindTodo(id int) Todo { for _, t := range todos { if == id { return t } } // return empty Todo if not found return Todo{} } func RepoCreateTodo(t Todo) Todo { currentId += 1 = currentId todos = append(todos, t) return t } func RepoDestroyTodo(id int) error { for i, t := range todos { if == id { todos = append(todos[:i], todos[i+1:]...) return nil } } return ("Could not find Todo with id of %d to delete", id) }
Add ID to Todo
We created a simulation database, we used and assigned id, so we also need to update our Todo structure accordingly.
package main import "time" type Todo struct { Id int `json:"id"` Name string `json:"name"` Completed bool `json:"completed"` Due `json:"due"` } type Todos []Todo
Update our TodoIndex
To use the database, we need to retrieve the data in TodoIndex. Modify the code as follows:
func TodoIndex(w , r *) { ().Set("Content-Type", "application/json; charset=UTF-8") () if err := (w).Encode(todos); err != nil { panic(err) } }
POST JSON
So far we've just outputted JSON, and it's time to go into storing some JSON.
Add the following route to the file:
Route{ "TodoCreate", "POST", "/todos", TodoCreate, },
Create routing
func TodoCreate(w , r *) { var todo Todo body, err := ((, 1048576)) if err != nil { panic(err) } if err := (); err != nil { panic(err) } if err := (body, &todo); err != nil { ().Set("Content-Type", "application/json; charset=UTF-8") (422) // unprocessable entity if err := (w).Encode(err); err != nil { panic(err) } } t := RepoCreateTodo(todo) ().Set("Content-Type", "application/json; charset=UTF-8") () if err := (w).Encode(t); err != nil { panic(err) } }
First we open the requested body. Pay attention to our use. This is a great way to protect your server from malicious attacks. What if someone wants to send 500GB of JSON to your server?
After we read the body, we deconstruct the Todo structure. If it fails, we make the correct response, using the appropriate response code 422, but we still use json to respond back. This allows the client to understand that something is happening, and there is a way to know what exactly happened.
Finally, if all pass, we respond to the 201 status code, indicating that the entity requested to create has been successfully created. We also respond to the json representing the entity we created, which will contain an id, which the client may need to use next.
POST some JSON
We now have a pseudo repo and create route, so we need to post some data. We use curl to achieve this goal through the following command:
If you access it again via http://localhost:8080/todos, you will probably get the following response:
[ { "id": 1, "name": "Write presentation", "completed": false, "due": "0001-01-01T00:00:00Z" }, { "id": 2, "name": "Host meetup", "completed": false, "due": "0001-01-01T00:00:00Z" }, { "id": 3, "name": "New Todo", "completed": false, "due": "0001-01-01T00:00:00Z" } ]
Things we haven't done yet
Although we have already had a good start, there are still many things we haven't done:
- Version control: What if we need to modify the API and the result changes completely? Maybe we need to add /v1/prefix to the beginning of our route?
- Authorization: Unless these are public/free APIs, we may also need authorization. It is recommended to learn JSON web tokens stuff.
eTag - If you are building something that needs to be extended, you may need to implement eTag.
what else?
For all projects, it started out small, but quickly became out of control. But if we want to take it to another level and make it production ready, there are some extra things to do:
- Refactoring a lot.
- Create several packages for these files, such as some JSON assistants, modifiers, processors, etc.
- Testing makes it impossible for you to forget this. We did not do any tests here. For production systems, testing is a must.
source code:/corylanou/tns-restful-json-api
Summarize
The most important thing to me is that we need to build a responsible API. Send appropriate status codes, headers, etc., these are the key to the widespread adoption of the API. I hope this article will let you start your own API as soon as possible.
Reference link
Go language RESTful JSON API implementation
JSON API
Gorilla Web Toolkit
httprouter
JSON Web Tokens
eTag
The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.