SoFunction
Updated on 2025-03-03

Detailed explanation of Go language RESTful JSON API creation

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:

  1. : Program entry file.
  2. : Routing-related processor.
  3. : Routing.
  4. : 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:

Copy the codeThe code is as follows:
curl -H "Content-Type: application/json" -d '{"name": "New Todo"}' http://localhost:8080/todos

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:

  1. 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?
  2. 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:

  1. Refactoring a lot.
  2. Create several packages for these files, such as some JSON assistants, modifiers, processors, etc.
  3. 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.