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
- 967