Golang, API, Application Architecture, REST

How to Create a RESTful API With Only The Golang Standard Library

Golang is a relatively new language which has really been gaining traction over the past couple years. It is powerful and has excellent tooling to design fast, efficient APIs. There are many libraries out there like Go Buffalo and Goa to create an API, but I thought it would be interesting to show you how to create one using only the standard library, no external dependencies besides database and cache connectors.

In this post, I will break down how to create an endpoint with the golang standard library. The whole API, including multiple endpoints, are on GitHub in my golang-standard-lib-rest-api repository.

Getting Started

The first step we're going to do is figure out your folder structure. We're going to need folders for our controllers, routes, requests, database migrations, database queries (repositories), and helper utils.

Your folder structure should look something like this.

controllers
database
models
repositories
requests
routes
utils

After you create those folders, let's create main.go. This is the first code we're writing so let's create it the way we want it to look. Then we will build the surrounding packages.

So, we want the main.go file to do a couple things for us.

  1. Create a database connection
  2. Create a caching connection for authentication
  3. Create a mux
  4. Load in our controllers
  5. Create our routes with the mux and controllers
  6. Start the server
func main() {
  db, err := database.Connect(os.Getenv("PGUSER"), os.Getenv("PGPASS"), os.Getenv("PGDB"), os.Getenv("PGHOST"), os.Getenv("PGPORT"))
  if err != nil {
    log.Fatal(err)
  }
  cache := &caching.Redis{
    Client: caching.Connect(os.Getenv("REDIS_ADDR"), os.Getenv("REDIS_PASSWORD"), 0),
  }

  mux := http.NewServeMux()

  userController := controllers.NewUserController(db, cache)
  jobController := controllers.NewJobController(db, cache)
  routes.CreateRoutes(mux, userController, jobController)

  if err := http.ListenAndServe(":8000", mux); err != nil {
    log.Fatal(err)
  }
}

We want it to look something like this. It creates our database and cache connections, loads our controllers, connects them to routes, and finally starts the server. Now that we have the main file figured out we have to create the packages we are using it.

Connection Utils

Let's start with the relatively simple packages, the database and caching utils.

The database.go file simply creates the connection string and opens a connection.
utils/database.go

func Connect(user, password, dbname, host, port string) (*sql.DB, error) {
  connStr := fmt.Sprintf("user=%s password=%s dbname=%s host=%s port=%s",
    user, password, dbname, host, port)
  return sql.Open("postgres", connStr)
}

The caching package is a bit more work as we are using an interface so if you decide to change to another cache the chance will be straightforward.
utils/caching/caching.go
This may change on your needs, but in this use case a good interface for a cache is something like this.

type Cache interface {
  Get(key string) (string, error)
  Set(key, value string, expiration time.Duration) error
}

Now create a structure which will match the interface

type Redis struct {
  Client *redis.Client
}
func (r *Redis) Get(key string) (string, error) {
  return r.Client.Get(key).Result()
}

func (r *Redis) Set(key, value string, expiration time.Duration) error {
  return r.Client.Set(key, value, expiration).Err()
}

And finally a function which will return the redis client.

func Connect(addr, password string, db int) *redis.Client {
   return redis.NewClient(&redis.Options{
    Addr:     addr,
    Password: password,
    DB:       db,
  })
}

User Controller

Okay, so we have the caching and database utils completed! Now we get to start working on the fun stuff. Let's define the routes we need. We're going to need two controllers, a user, and a job controller. And each of those controllers will have a few endpoints. Let's write these down before actually writing the controllers out.

User Controller

POST /register
POST /login
Job Controller

GET /job/{id}
PUT /job/{id}
DELETE /job/{id}
POST /job
GET /feed

After doing this, we now know which endpoints we need. Let's create the user controller and make the registration endpoint.

If you look at main.go you'll see there is a method called NewUserController, that is a good place to start. We want to be able to access the cache and database from within the user controller, so we're going to pass those into the function.

The struct needs to look like this, so we are able to call the database and cache like userController.DB and userController.Cache inside each controller method. Note Cache is an interface so we can swap it out at any time.

 type UserController struct {
   DB    *sql.DB
   Cache caching.Cache
 }

The NewUserController function should simply return a UserController struct with a database and cache.

func NewUserController(db *sql.DB, c caching.Cache) *UserController {
  return &UserController{
    DB:    db,
    Cache: c,
  }
}

Registration Endpoint

Great so we have a user controller, now let's add a method. Registering is essential, so let's write that one out, and we can walk through each step.

First, we only want POST requests on this endpoint, so if check if the method is POST, and if not send a not found status to the client.

 func (jc *UserController) Register(w http.ResponseWriter, r *http.Request) {
   if r.Method != "POST" {
     http.Error(w, "Not found", http.StatusNotFound)
     return
   }

After that is checked, you want to decode the request body and retrieve the request data. If the body is malformed, we send back a bad request status. The struct will be defined later in the post.

   decoder := json.NewDecoder(r.Body)
   var rr requests.RegisterRequest
   err := decoder.Decode(&rr)
   if err != nil {
     http.Error(w, "Invalid request body", http.StatusBadRequest)
     return
   }

Now that we have the request data in a struct we can create the user, so we pass the data into the repository which will create the user and return their ID. If there is an error at this stage, we return an internal server error.

   id, err := repositories.CreateUser(jc.DB, rr.Email, rr.Name, rr.Password)
   if err != nil {
     log.Fatalf("Add user to database error: %s", err)
     http.Error(w, "", http.StatusInternalServerError)
     return
   }

Finally, once the user has been saved, we generate a random token and set it to be a key in our Redis cache, we set the value to be the user id so we can authenticate the user when they make subsequent requests.

   token, err := crypto.GenerateToken()
   if err != nil {
     log.Fatalf("Generate token Error: %s", err)
     http.Error(w, "", http.StatusInternalServerError)
     return
   }
   oneMonth := time.Duration(60*60*24*30) * time.Second
   err = jc.Cache.Set(fmt.Sprintf("token_%s", token), strconv.Itoa(id), oneMonth)
   if err != nil {
     log.Fatalf("Add token to redis Error: %s", err)
     http.Error(w, "", http.StatusInternalServerError)
     return
   }

The last step is to return the token to the user, and set the content type to json.

   p := map[string]string{
     "token": token,
   }
   w.Header().Set("Content-Type", "application/json")
   json.NewEncoder(w).Encode(p)
 }

Final Pieces

Now the controller and endpoint are complete. But you notice that a couple pieces are missing, firstly we need a request object to get the request body data.

type RegisterRequest struct {
  Email    string `json:"email"`
  Name     string `json:"name"`
  Password string `json:"password"`
}

The last step is to create a user repository. Once you create the file repositories/user_repository.go we need to add a CreateUser function. It will generate a salt, hash the password, and save all the data in the user's table.

func CreateUser(db *sql.DB, email, name, password string) (int, error) {
  const query = `
    insert into users (
      email,
      name,
      password,
      salt
    ) values (
      $1,
      $2,
      $3,
      $4
    ) returning id
  `
  salt := crypto.GenerateSalt()
  hashedPassword := crypto.HashPassword(password, salt)
  var id int
  err := db.QueryRow(query, email, name, hashedPassword, salt).Scan(&id)
  return id, err
}

There is a whole crypto section being used throughout this post, you can find this code here. It is not an essential part of this blog post but feels free to use it.

Conclusion

This controller is just a piece of an API I made to show how easily you can create an API using Golang with only the standard library. Golang is a great language to build APIs and microservices, and the performance gains over languages like Javascript and PHP make using Golang a no-brainer. Although I did this with only the standard library, I think using a couple external libraries like Gorilla Mux and go-validator will make your life easier, and help make your code more readable and maintainable.

Author image

About Ryan McCue

Hi, my name is Ryan! I am a Software Developer with experience in many web frameworks and libraries including NodeJS, Django, Golang, and Laravel.