RESTful API Development with Go

 Code Camp NYC 2019

Jyotsna Gupta

  • Software Developer at Exotel
     
  • Open Source Enthusiast
       - Mozilla Tech Speaker
       - Mozilla Representative
       - Contributes to Add-ons Community at Mozilla

 

@imJenal

Agenda

  • APIs
  • Intro to REST
  • Creating Endpoints
  • HTTP Methods
  • Connecting with Mongo
  • Securing API
  • Writing Tests
  • Best Practises
Image Source: www.calhoun.io/

@imJenal

Application Programming Interface

API

@imJenal

@imJenal

  • REpresentational State Transfer
     
  • Streamlined and lightweight web service
     
  • Core Principles: Performance, scalability, simplicity, portability
     
  • REST is an architectural style, or design pattern, for APIs.

REST, RESTful

@imJenal

REST
CLIENT

REST
SERVER

Why REST in Go?

@imJenal

Because of Go

Image Source: www.uihere.com

Let's Go

Image Source: www.deviantart.com/

@imJenal

Basic Example

@imJenal

package main

import (
    "net/http"
)


func main() {
   http.ListenAndServe(":3000", nil)
}

main.go

Gorilla Mux

@imJenal

  • HTTP request multiplexer
     
  • A powerful URL router and dispatcher
     
  • matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL
     
import "github.com/gorilla/mux"

Router

@imJenal

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "github.com/gorilla/mux"
)

func main() {
    router := mux.NewRouter()
    router.HandleFunc("/", HomeHandler)
    router.HandleFunc("/movie/{id}", MovieHandler)
    log.Fatal(http.ListenAndServe(":3000", router))
}
main.go

HTTP Methods

@imJenal

C R U D

Create

Read

Update

Delete

GET

PUT

POST

DELETE

func AllMoviesEndPoint(w http.ResponseWriter, r *http.Request) {}
func FindMovieEndpoint(w http.ResponseWriter, r *http.Request) {}
func CreateMovieEndPoint(w http.ResponseWriter, r *http.Request) {}
func UpdateMovieEndPoint(w http.ResponseWriter, r *http.Request) {}
func DeleteMovieEndPoint(w http.ResponseWriter, r *http.Request) {}

func main() {
	router := mux.NewRouter()
	router.HandleFunc("/movies", AllMoviesEndPoint).Methods("GET")
	router.HandleFunc("/movies", CreateMovieEndPoint).Methods("POST")
	router.HandleFunc("/movies", UpdateMovieEndPoint).Methods("PUT")
	router.HandleFunc("/movies", DeleteMovieEndPoint).Methods("DELETE")
	router.HandleFunc("/movies/{id}", FindMovieEndpoint).Methods("GET")
	log.Fatal(http.ListenAndServe(":3000",router))
}

@imJenal

@imJenal

How To Pass Data

Connecting with Mongo

package dao

import (
	"log"
	mgo "gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"
)

type MoviesDAO struct {
	Server   string
	Database string
}

var db *mgo.Database

const (COLLECTION = "movies")

func (m *MoviesDAO) Connect() {
	session, err := mgo.Dial(m.Server)
	if err != nil {
		log.Fatal(err)
	}
	db = session.DB(m.Database)
}

@imJenal

dao.go
type Movie struct {
	ID          string        `bson:"_id"`
	Name        string        `json:"name"`
	CoverImage  string        `json:"cover_image"`
	Description string        `json:"description"`
}

@imJenal

model.go

Basic model : Movie

func (m *MoviesDAO) FindAll() ([]Movie, error) {
	var movies []Movie
	err := db.C(COLLECTION).Find(bson.M{}).All(&movies)
	return movies, err
}

func (m *MoviesDAO) FindById(id string) (Movie, error) {
	var movie Movie
	err := db.C(COLLECTION).FindId(bson.ObjectIdHex(id)).One(&movie)
	return movie, err
}

func (m *MoviesDAO) Insert(movie Movie) error {
	err := db.C(COLLECTION).Insert(&movie)
	return err
}

func (m *MoviesDAO) Delete(movie Movie) error {
	err := db.C(COLLECTION).Remove(&movie)
	return err
}

func (m *MoviesDAO) Update(movie Movie) error {
	err := db.C(COLLECTION).UpdateId(movie.ID, &movie)
	return err
}

@imJenal

func FindMovieEndpoint(w http.ResponseWriter, r *http.Request) {
	params := mux.Vars(r)
	movie, err := dao.FindById(params["id"])
	if err != nil {
		respondWithError(w, http.StatusBadRequest, "Invalid Movie ID")
		return
	}
	respondWithJson(w, http.StatusOK, movie)
}

@imJenal

@imJenal

Securing Endpoints

JWT

@imJenal

func main() {
    router := mux.NewRouter()
    allowedHeaders := handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"})
    
    allowedMethods := handlers.AllowedMethods([]string{"GET", "POST", "PUT", "HEAD", "OPTIONS"})
    
    allowedOrigins := handlers.AllowedOrigins([]string{*})
    
    log.Fatal(http.ListenAndServe(":3000", handlers.CORS(allowedHeaders, allowedMethods, allowedOrigins)(router)))
}

CORS

@imJenal

Writing Tests

@imJenal

package main

func Add(value1 int, value2 int) int {
    return value1 + value2
}

func main() { }
main.go

@imJenal

package main

import (
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestAdd(t *testing.T) {
    total := Add(1, 3)
    assert.NotNil(t, total, "The `total` should not be `nil`")
    assert.Equal(t, 4, total, "Expecting `4`")
}

main_test.go

Test endpoints

@imJenal


func TestEndpointGET(t *testing.T) {
    request, _ := http.NewRequest("GET", "/create", nil)
    response := httptest.NewRecorder()
    router := mux.NewRouter()
    router.Handle("/create", EndpointGET).Methods("GET")
    router.ServeHTTP(response, request)
    assert.Equal(t, 200, response.Code, "OK response is expected")
}

Endpoint with GET

@imJenal

func TestEndpointPOST(t *testing.T) {
    person := &Person{
        Firstname: "Nic",
        Lastname: "Raboy"
    }
    jsonPerson, _ := json.Marshal(person)
    request, _ := http.NewRequest("POST", "/create", bytes.NewBuffer(jsonPerson))
    response := httptest.NewRecorder()
    router := mux.NewRouter()
    router.Handle("/create", EndpointPOST).Methods("POST")
    router.ServeHTTP(response, request)
    assert.Equal(t, 200, response.Code, "OK response is expected")
}

Endpoint with POST

@imJenal

Best Practices

@imJenal

Abstract vs Concrete APIs

For the REST APIs,
the concrete is better than abstract

/entities

  • /owners
  • /blogs
  •  /blogposts

Bad URL

  • GET /getMovies – gets all the movies
  • GET /getMovieById/12 – gets the movies with the id 12
  • POST /addMovies – adds a new movie and returns the details
  • DELETE /deleteMovies/12 – removes the movies with the id 12
  • GET /getMoviesByActorId/3/– gets all the movies of the actor with id 3

Good URL

  • GET /movies – gets all the movies
  • GET /movies/12 – gets the movies with the id 12
  • POST /movies – adds a new movie and returns the details
  • DELETE /movies/12 – removes the movies with the id 12
  • GET /actor/3/movies – gets all the movies of the actor with id 3

Error Handling

  • Request: GET https://api.twitter.com/1.1/account/settings.json
     
  • Response: Status Code 400
{"errors":[{"code":215,"message":""Bad Authentication data"}]}

Status Codes

  • 200 OK : Everything is working
  • 201 OK : New resource has been created
  • 400 Bad Request: Request can't be served
  • 404 Not Found : No resource behind the URI
  • 500 Internal Server Error

REST API Versioning

https://api.example.com/v1/authors/2/movies/13

Documentation

@imJenal

Slides: https://slides.com/jenal/codecampnyc-go 

RESTful API Development using Go

By Jyotsna Gupta

RESTful API Development using Go

Code Camp New York | October 12, 2019

  • 840