Ya os hemos explicado como crear un command line en Go utilizando los paquetes que nos ofrece Go en el propio lenguaje, sin necesidad de ninguna librería externa.
Pues esta vez crearemos nuestra primera API Rest, exclusivamente con lo que Go nos permite de entrada, bueno veremos que esto no es exactamente cierto, haremos algo de trampa y utilizaremos alguna librería externa, pero ningún framework.
¿Por qué no usamos un framework?
Si bien es cierto que existen bastantes frameworks para crear APIs en Go en la mayoría de los casos no es necesario, pensad que cuando recurrimos a Go para realizar un proyecto es por el hecho de que es más ligero y rápido que otros lenguajes actuales, pero si le empezamos a añadir capas innecesarias, no nos estaremos beneficiando de este punto.
Arquitectura del proyecto
Vamos a darle algo de forma a nuestro proyecto.
cmd
- gopherapi
-- main.go
pkg
- server
LICENSE
README.md
Como en otras ocasiones nuestro cmd/gopherapi/main.go
será el punto con el que arrancaremos nuestra aplicación en este caso una API
Dentro de pkg
encapsularemos el resto del código, si habéis echado un vistazo a otros proyectos de GO muchas veces todo el código se encuentra a nivel del directorio raíz, esto es o bien porque son paquetes con lo cual se sobre entiende que estás dentro del paquete en sí, o son proyectos muy simples.
El directorio pkg, es una manera de agrupar tu código cuando el directorio raíz contiene demasiados ficheros o directorios, haciendo sencillo ejecutar diversas go tools.
Además si tu proyecto es público muchos developers esperan encontrar dentro del directorio pkg todo lo necesario para poder importar tu aplicación en sus proyectos.
Dentro de pkg/server
encontraremos todo lo necesario para que nuestro server HTTP funcione, así como los handlers correspondientes.
net/http es tu amigo
Vamos a inicializar nuestro servidor, aquí seguro que esperaréis una explicación super compleja de como levantar nuestro servidor HTTP, bueno veamos como queda nuestro archivo cmd/gopherapi/main.go
package main
import (
"log"
"net/http"
)
func main() {
log.Fatal(http.ListenAndServe(":8080", nil))
}
Sí, sí, no os engaño, hagamos la prueba, arranquemos el proyecto en una terminal:
$ go run main.go
Y en otra terminal probad:
$ curl http://localhost:8080
404 page not found
Como veis está funcionando correctamente, nos devuelve 404 porque no tenemos ningún handler asociado a nuestro servidor.
El router de Gorilla
Como hemos visto anteriormente si lanzamos nuestro código así sin más, lo único que obtendremos es un 404, pero nosotros hemos venido aquí a montar nuestra primera API Rest, así que, ¿cómo lo hago?
Para empezar esta parte vamos a aprovecharnos de una librería realmente útil, Gorilla Mux, realmente podríamos realizar los endpoints sin ella, pero cuenta con ciertas funcionalidades que nos facilitarán mucho la vida.
$ go get -u github.com/gorilla/mux
Vamos a escribir nuestro fichero pkg/server/api.go
normalmente, cada handler tendría su propio fichero, pero para mantenernos simples crearemos todos nuestros endpoints en api.go
.
package server
import "github.com/gorilla/mux"
type api struct {
router http.Handler
}
type Server interface {
Router() http.Handler
}
func New() Server {
a := &api{}
r := mux.NewRouter()
a.router = r
return a
}
func (a *api) Router() http.Handler {
return a.router
}
Ahora modificaremos nuestro fichero cmd/gopherapi/main.go
para que tenga en cuenta la nueva configuración de server que hemos creado.
package main
import (
"log"
"net/http"
"github.com/friendsofgo/gopher-api/pkg/server"
)
func main() {
s := server.New()
log.Fatal(http.ListenAndServe(":8080", s.Router()))
}
¿Qué hemos hecho?
El paquete de Gorilla nos permite gestionar todos nuestros endpoints de una manera sencilla con un simple router, que es todo lo que necesita nuestro servidor para ser configurado.
Pero si ejecutamos ahora nuestro código seguirá funcionando igual:
$ curl http://localhost:8080
404 page not found
Y es que realmente tan sólo hemos añadido el router pero no hemos creado ningún endpoint
Nuestros primeros endpoints
Ha llegado hora de darle vida a nuestra API que es por lo que estamos aquí, vamos a crear los siguientes endpoints:
- GET
/gophers
| Devuelve todos los gophers - GET
/gophers/{gopher_id}
| Devuelve los datos del gopher seleccionado
Así que deberemos escribir nuestros respectivos handlers en este tutorial obviaremos el acceso al storage, pero como siempre os daremos el repositorio con el que hemos hecho el artículo al final, para que podáis echarle un ojo.
Creemos las rutas
Primero que nada vamos a decirle a nuestro router las rutas que tiene que empezar a resolver.
package server
import (
"net/http"
"github.com/gorilla/mux"
)
type api struct {
router http.Handler
}
type Server interface {
Router() http.Handler
}
func New() Server {
a := &api{}
r := mux.NewRouter()
r.HandleFunc("/gophers", a.fetchGophers).Methods(http.MethodGet)
r.HandleFunc("/gophers/{ID:[a-zA-Z0-9_]+}", a.fetchGopher).Methods(http.MethodGet)
a.router = r
return a
}
func (a *api) Router() http.Handler {
return a.router
}
Gracias a Gorilla podemos usar expresiones regulares para asegurarnos de antemano que los parámetros pasados cumplen con la regla que queremos.
r.HandleFunc("/gophers/{ID:[a-zA-Z0-9_]+}", a.fetchGopher).Methods(http.MethodGet)
¿Qué pasa si ejecutamos nuestro código ahora?
$ go run cmd/gopherapi/main.go
pkg/server/api.go:21:28: a.fetchGophers undefined (type *api has no field or method fetchGophers)
pkg/server/api.go:22:47: a.fetchGopher undefined (type *api has no field or method fetchGopher)
¡Claro! Nos falta crear las respectivas funciones de los handlers de momento creémoslas vacías.
...
func (a *api) fetchGophers(w http.ResponseWriter, r *http.Request) {}
func (a *api) fetchGopher(w http.ResponseWriter, r *http.Request) {}
...
Si volvemos a ejecutar nuestro código funcionará, pero nuestros endpoints continuan estando vacíos, así que tampoco hay mucho que ver ¿no?
Como podemos ver las funciones esperan dos parámetros w
y r
que son del tipo http.ResponseWriter
y http.Request
respectivamente. Estos dos parámetros tendrán datos una vez realicemos la petición al endpoint al cual hemos vinculado la función en cuestión.
Empecemos a mostrar datos
Para este ejemplo hemos creado un simple struct que define a un Gopher dentro del fichero pkg/gopher.go
package gopher
// Gopher defines the properties of a gopher to be listed
type Gopher struct {
ID string `json:"ID"`
Name string `json:"name,omitempty"`
Image string `json:"image,omitempty"`
Age int `json:"age,omitempty"`
}
En nuestro struct
podemos ver que después de declarar el tipo hay algo más, ¿qué es esa anotación? Pues son tags
, con esto ahorraremos muchísimo código que en otros lenguajes tenemos que realizar muchas veces a mano.
Lo que hará esta anotación de manera automática tanto cuando se ejecute la función json.Marshal
como json.Unmarshal
es transformar nuestro struct, ya sea desde json o hacia json, además nos permite indicarle como vemos, que pasa cuando el dato viene vacío. Se pueden realizar más acciones pero de momento con esto será suficiente.
Como dijimos anteriormente, daremos por hecho que nuestra aplicación contiene un repositorio con datos a mostrar. Así pues veamos como implementamos los handlers.
...
func (a *api) fetchGophers(w http.ResponseWriter, r *http.Request) {
gophers, _ := a.repository.FetchGophers()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(gophers)
}
func (a *api) fetchGopher(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
gopher, err := a.repository.FetchGopherByID(vars["ID"])
w.Header().Set("Content-Type", "application/json")
if err != nil {
w.WriteHeader(http.StatusNotFound) // We use not found for simplicity
json.NewEncoder(w).Encode("Gopher Not found")
return
}
json.NewEncoder(w).Encode(gopher)
}
...
Obviamente en un código real tendríamos una mejor implementación de las respuestas y errores, pero con estos ejemplos podréis ver como funcionan a grandes rasgos los handlers.
Quizás os resulte algo raro el término handler o request handler que es como se les conoce en Go, pero seguro que si los llamo controller o controladores, que corresponde al controlador (C) de la arquitectura MVC, pasaréis a comprender mejor que es lo que hemos realizado en el código anterior.
Ahora vamos a probar nuestra aplicación:
Primero recordemos en un terminal ejecutamos el server:
$ go run cmd/gopherapi/main.go
Y en otra probaremos las llamadas:
$ curl http://localhost:8080/gophers
[
{"ID":"01D3XZ3ZHCP3KG9VT4FGAD8KDR","name":"Jenny","image":"https://storage.googleapis.com/gopherizeme.appspot.com/gophers/0ceb2c10fc0c30575c18ff1defa1ffd41501bc62.png","age":18},
{"ID":"01D3XZ7CN92AKS9HAPSZ4D5DP9","name":"Billy","image":"https://storage.googleapis.com/gopherizeme.appspot.com/gophers/13c7d425111a501600db8587b52bb292836c5bee.png","age":24},
{"ID":"01D3XZ89NFJZ9QT2DHVD462AC2","name":"Rainbow","image":"https://storage.googleapis.com/gopherizeme.appspot.com/gophers/b9e8d637c91c089fd56d7b159825fc9089377118.png","age":48},
{"ID":"01D3XZ8JXHTDA6XY05EVJVE9Z2","name":"Bjorn","image":"https://storage.googleapis.com/gopherizeme.appspot.com/gophers/fd01b36091560c2a128b8fddfb2c627d8bb7417c.png","age":32}
]
¡Asombroso!
$ curl http://localhost:8080/gophers/01D3XZ3ZHCP3KG9VT4FGAD8KDR
{"ID":"01D3XZ3ZHCP3KG9VT4FGAD8KDR","name":"Jenny","image":"https://storage.googleapis.com/gopherizeme.appspot.com/gophers/0ceb2c10fc0c30575c18ff1defa1ffd41501bc62.png","age":18}
¡Maravilloso!
Testing
Ahora el punto que deberíamos tratar sería como testear nuestra API, pero esto lo dejaremos para un siguiente artículo, que obviamente encontraréis en el blog.
Conclusión
Has aprendido como realizar una API RESTful usando GO. De momento hemos tenido que mockear nuestros datos y hemos obviado la parte de como guardar en base de datos, y como realizar los tests, pero no perdáis de vista esta API porque seguiremos evolucionándola en próximos artículos.
Aquí os dejamos el repositorio de la misma: https://github.com/friendsofgo/gopherapi/
Además ya sabéis que me gustan mucho los retos, ¿quién se atreve a añadir los endpoints que nos faltan?
- POST
/gophers
| Crea un nuevo gopher - PUT
/gophers/{gopher_id}
| Modifica un gopher existente - DELETE
/gophers/{gopher_id}
| Elimina el gopher pasado por ID
El repositorio ya tiene los métodos necesarios para realizar dichas acciones así que sólo tendríais que completar los handlers.
Si os animáis haced un Pull Request con vuestra propuesta, la mejor será añadida en el repositorio.