2. Comenzado

En este post vamos a iniciar el directorio del proyecto y estableceremos las bases para construir nuestra API. Haremos lo siguiente:

  1. Crear una estructura de directorios para el proyecto y explicar a alto nivel cómo se organizarán nuestro código en Go y otros recursos.
  2. Establecer un servidor HTTP para escuchar las solicitudes HTTP entrantes.
  3. Introducir un patrón sensato para gestionar la configuración (a través de banderas en la línea de comandos) y utilizar la inyección de dependencias para hacer que las dependencias estén disponibles para nuestros controladores.
  4. Utilizar el paquete httprouter para ayudar a implementar una estructura RESTful estándar para los puntos finales de la API.

2.1 Setup del proyecto y estructura

Primero creemos una carpeta llamada greenlight luego hacemos el go mod init dentro de la carpeta en mi caso go mod init github.com/nahueldev23/greenlight.

Se deberia haber creado el fichero go.mod

2.1 Generando la estructura del proyecto

Para generar la estructura basica ejecutemos el siguiente script dentro de la carpeta que creamos recien.

$ mkdir -p bin cmd/api internal migrations remote
$ touch Makefile
$ touch cmd/api/main.go

En este punto el directorio se deberia ver asi:

.
├── bin
├── cmd
│ └── api
│ └── main.go
├── internal
├── migrations
├── remote
├── go.mod
└── Makefile
  • El directorio bin contendra los binarios compilados de nuestra app, la cual esta lista para hacer un deploy a un server de produccion.
  • El directorio cmd/api contendra el codigo especifico de nuestra app. Esto incluye el codigo para correr el servidor, leer y escribir peticiones HTTP y el manejo de autenticacion.
  • El directorio internal contendra varios paquetes auxiliares que usa nuestra API. Contendra ademas codigo que va a interactuar con nuestra DB, haciendo validaciones de datos, enviar emails,etc.Basicamente, cualquier codigo el cual no sea especifico de nuestra app y que potencialmente podamos reutilizar. Nuestro codigo de Go que este bajo cmd/api importara los paquetes de internal pero nunca al reves.
  • El directorio migrations contendra los ficheros de las migraciones SQL.
  • El directorio remote contendra la configuración de los archivos y los scripts de setup para nuestro servidor de produccion.
  • go.mod contendra las dependencias de nuestro proyecto, las versiones y el path del modulo.
  • Makefile contendra el recipiente para tareas administrativas comunes, como auditar nuestro codigo , construir binarios , y ejecutar migraciones a las DB.

En Go la carpeta internal tiene un rol importante,Basicamente lo que sea que tengamos dentro de el, solo puede ser importado por otros directios de nuestro propio proyecto. En otras palabras cualquier paquete que este alli dentro no puede ser importado fuera de nuetro proyecto original.

2.1 Hello world

Abrimos cmd/api/main.go y agregamos lo siguiente:

package main

import "fmt"

func main() {
	fmt.Println("Hola mundo")
}

Guardamos los cambios y ejecutemos go run ./cmd/api

$ go run ./cmd/api
Hello world!

2.2 Servidor Basico HTTP

Crearemos un solo endpoit por ahora : /v1/healthcheck. Este endpoit retornara infomacion basica sobre nuestra API, incluyendo nuestro numero de version actual y el entorno operativo (development,staging,production,etc).

URL PatternHandlerAction
/v1/healthcheckhealthcheckHandlerShow application information
package main

import (
	"flag"
	"fmt"
	"net/http"
	"os"
	"time"

	"log/slog"
)

const version = "1.0.0"

// Define a config struct to hold all the configuration settings for our application.
// For now, the only configuration settings will be the network port that we want the
// server to listen on, and the name of the current operating environment for the
// application (development, staging, production, etc.). We will read in these
// configuration settings from command-line flags when the application starts.
type config struct {
	port int
	env  string
}

type application struct {
	config config
	logger *slog.Logger
}

func main() {
	var cfg config

	// Read the value of the port and env command-line flags into the config struct. We
	// default to using the port number 4000 and the environment "development" if no
	// corresponding flags are provided.
	flag.IntVar(&cfg.port, "port", 4000, "API server port")
	flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)")
	flag.Parse()

	// Initialize a new structured logger which writes log entries to the standard out
	// stream.
	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

	app := application{
		config: cfg,
		logger: logger,
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/v1/healthcheck", app.healhtcheckHandler)

	// Declare a HTTP server which listens on the port provided in the config struct,
	// uses the servemux we created above as the handler, has some sensible timeout
	// settings and writes any log messages to the structured logger at Error level.
	srv := &http.Server{
		Addr:         fmt.Sprintf(":%d", cfg.port),
		Handler:      mux,
		IdleTimeout:  time.Minute,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
		ErrorLog:     slog.NewLogLogger(logger.Handler(), slog.LevelError),
	}

	logger.Info("starting server", "addr", srv.Addr, "env", cfg.env)

	err := srv.ListenAndServe()
	logger.Error(err.Error())
	os.Exit(1)
}

2.2 Creando el handler healthcheck

Por ahora esponderemos tres cosas en texto plano:

package main

import (
	"fmt"
	"net/http"
)

// Declare a handler which writes a plain-text response with information about the
// application status, operating environment and version.
func (app *application) healthcheckHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "status: available")
	fmt.Fprintf(w, "environment: %s\n", app.config.env)
	fmt.Fprintf(w, "version: %s\n", version)
}

Lo importante aca es que healthcheck es un metodo de la estructura application.

Esta es una manera de hacer que las dependencias esten disponibles en nuestro handlers sin recurrir a variables globales o a closures. Cualquier dependecia que necesite healthcheck puede simplemente ser incluida como un campo de la estructura application.

En el codigo anterior podemos ver su uso en app.config.env

2.2 Demostracion

Iniciamos el servidor go run ./cmd/api, luego vamos al navegador en localhost:4000/v1/healthcheck y podremos ver la respuesta en texto plano.

O podemso usar curl

$ curl -i localhost:4000/v1/healthcheck
HTTP/1.1 200 OK
Date: Mon, 05 Apr 2021 17:46:14 GMT
Content-Length: 58
Content-Type: text/plain; charset=utf-8
status: available
environment: development
version: 1.0.0

Nota: El indicador -i en el comando anterior indica a curl que muestre la respuesta HTTP encabezados y el cuerpo de la respuesta.

Si queremos ver si los flags estan funcionando lo podemos revisar asi :

$ go run ./cmd/api -port=3030 -env=production
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:3030 env=production

2.2 Informacion adicional

2.2 API version

Las API que respaldan negocios y usuarios del mundo real a menudo necesitan cambiar su funcionalidad y puntos finales con el tiempo, a veces de una manera no compatible con versiones anteriores. Por lo tanto, para evitar problemas y confusiones para los clientes, es una buena idea implementar siempre alguna forma de versionamiento de la API.

Yo usare el prefijo en las urls /v1/healthcheck o /v2/healthcheck

Aunque hay otros enfoques.

2.3 API Endpoints y RESTful Routing

Mas adelante vamos a ir creando nuestros endpoits de nuestra API y se vera asi:

MethodURL PatternHandlerAction
GET/v1/healthcheckhealthcheckHandlerShow application information
GET/v1/movieslistMoviesHandlerShow the details of all movies
POST/v1/moviescreateMovieHandlerCreate a new movie
GET/v1/movies/:idshowMovieHandlerShow the details of a specific movie
PUT/v1/movies/:ideditMovieHandlerUpdate the details of a specific movie
DELETE/v1/movies/:iddeleteMovieHandlerDelete a specific movie

2.3 Eligiendo un router

Hasta ahora estuvimos usando http.ServeMux pero es un poco limitado para lo que queremos hacer y de la manera que queremso llevarlo a cabo. Nosotros buscamos que se dispare cada handler dependiendo del verbo HTTP que recibamos ademas del path al que se haga la peticion. Tambien queremos manejar clean URLs lo cual no nos lo provee http.ServeMux.

En este post vamos a integrar un paquete de terceros llamado httprouter el cual es estable,bien testeado, provee la funcionalidad que buscamos y es muy rapido.Si estas creando una api para un consumo publico, entonces httproute es una buena opcion.

Descargemoslo con go get

$ go get github.com/julienschmidt/httprouter@v1
go: downloading github.com/julienschmidt/httprouter v1.3.0
go get: added github.com/julienschmidt/httprouter v1.3.0

Para demostrar como funciona httprouter_ vamos a comenzar creando dos endpoints, uno para crear una nueva pelicula y otro para mostrar los detalles de una pelicula especifica. Al final de la seccion nuestros endpoints se veran asi:

MethodURL PatternHandlerAction
GET/v1/healthcheckhealthcheckHandlerShow application information
GET/v1/movieslistMoviesHandlerShow the details of all movies
POST/v1/moviescreateMovieHandlerCreate a new movie

2.3 Encapsulando las rutas de la API

Para prevenir que nuetro main() crezca demasiado a medida que creamos las nuevas rutas vamos a encapular las reglas en cmd/api/routes.go.

touch cmd/api/routes.go 
package main

import (
	"github.com/julienschmidt/httprouter"
	"net/http"
)

func (app *application) routes() http.Handler {
	// Initialize a new httprouter router instance.
	router := httprouter.New()
	// Register the relevant methods, URL patterns and handler functions for our
	// endpoints using the HandlerFunc() method. Note that http.MethodGet and
	// http.MethodPost are constants which equate to the strings "GET" and "POST"
	// respectively.
	router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler)
	router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler)
	router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler)
	// Return the httprouter instance.
	return router
}

Tenemos dos ventajas por haber aislado de esta manera los routers. La primera es que mantenemos nuestro main() limpio y nos aseguramos de que tenemso todas las rutas en un solo lugar. La otra es que podemos acceder al router en cualqueir test con la inicializacion de una instanacia application y llamando al metodo routes().

Lo siguiente es actualizar main() para quitar el uso de http.ServeMux y usar en su lugar httproute.

  package main
  
  "flag"
  import (
  	"fmt"
  	"net/http"
  	"os"
  	"time"
  
  	"log/slog"
  )
  
  const version = "1.0.0"
  
  type config struct {
  	port int
  	env  string
  }
  
  type application struct {
  	config config
  	logger *slog.Logger
  }
  
  func main() {
  	var cfg config
  
  	flag.IntVar(&cfg.port, "port", 4000, "API server port")
  	flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)")
  	flag.Parse()
  
  	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
  
  	app := application{
  		config: cfg,
  		logger: logger,
  	}
  
-  	mux := http.NewServeMux()
-  	mux.HandleFunc("/v1/healthcheck", app.healthcheckHandler)
  
  	srv := &http.Server{
  
  		Addr:         fmt.Sprintf(":%d", cfg.port),
  		Handler:      app.routes,
  		IdleTimeout:  time.Minute,
  		ReadTimeout:  5 * time.Second,
  		WriteTimeout: 10 * time.Second,
  		ErrorLog:     slog.NewLogLogger(logger.Handler(), slog.LevelError),
  	}
  
  	logger.Info("starting server", "addr", srv.Addr, "env", cfg.env)
  
  	err := srv.ListenAndServe()
  	logger.Error(err.Error())
  	os.Exit(1)
  }

2.3 Agregando nuevos handler functions

Vamos a crear createMovieHandler y showMovieHandler.

touch cmd/api/movies.go
package main

import (
	"fmt"
	"github.com/julienschmidt/httprouter"
	"net/http"
	"strconv"
)

// Add a createMovieHandler for the "POST /v1/movies" endpoint. For now we simply
// return a plain-text placeholder response.
func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "create a new movie")
}

// Add a showMovieHandler for the "GET /v1/movies/:id" endpoint. For now, we retrieve
// the interpolated "id" parameter from the current URL and include it in a placeholder
// response.
func (app *application) showMovieHandler(w http.ResponseWriter, r *http.Request) {
	// When httprouter is parsing a request, any interpolated URL parameters will be
	// stored in the request context. We can use the ParamsFromContext() function to
	// retrieve a slice containing these parameter names and values.
	params := httprouter.ParamsFromContext(r.Context())
	// We can then use the ByName() method to get the value of the "id" parameter from
	// the slice. In our project all movies will have a unique positive integer ID, but
	// the value returned by ByName() is always a string. So we try to convert it to a
	// base 10 integer (with a bit size of 64). If the parameter couldn't be converted,
	// or is less than 1, we know the ID is invalid so we use the http.NotFound()
	// function to return a 404 Not Found response.
	id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
	if err != nil || id < 1 {
		http.NotFound(w, r)
		return
	}
	// Otherwise, interpolate the movie ID in a placeholder response.
	fmt.Fprintf(w, "show the details of movie %d\n", id)
}

Con eso listo podemos probar los endpoints.

$ go run ./cmd/api
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development

Abrimos otra terminal y revisamos.

$ curl localhost:4000/v1/healthcheck
status: available
environment: development
version: 1.0.0
$ curl -X POST localhost:4000/v1/movies
create a new movie
$ curl localhost:4000/v1/movies/

Si intentamos usar un verbo HTTP que no sea valido:

$ curl -i -X POST localhost:4000/v1/healthcheck
HTTP/1.1 405 Method Not Allowed
Allow: GET, OPTIONS
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Tue, 06 Apr 2021 06:59:04 GMT
Content-Length: 19
Method Not Allowed

2.3 Creando un helper para leer los parametros ID

Creamos cmd/api/helpers.go

$ touch cmd/api/helpers.go

Creamos un nuevo metodo readIDParam para nuestra estructura application.

package main

import (
	"errors"
	"github.com/julienschmidt/httprouter"
	"net/http"
	"strconv"
)

// Retrieve the "id" URL parameter from the current request context, then convert it to
// an integer and return it. If the operation isn't successful, return 0 and an error.
func (app *application) readIDParam(r *http.Request) (int64, error) {
	params := httprouter.ParamsFromContext(r.Context())
	id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
	if err != nil || id < 1 {
		return 0, errors.New("invalid id parameter")
	}
	return id, nil
}

Nota: El método readIDParam() no utiliza ninguna dependencia de nuestra estructura de aplicación, por lo que podría ser una función regular en lugar de un método en la estructura application. Sin embargo, en general, sugiero configurar todos los controladores y ayudantes específicos de la aplicación de manera que sean métodos en la estructura de la aplicación. Esto ayuda a mantener la consistencia en la estructura de tu código y también futuriza tu código para cuando esos controladores y ayudantes cambien más adelante y necesiten acceso a una dependencia.

Con el helper en su lugar, showMovieHandler puede ser mas simple.

  package main
  
  import (
  	"fmt"
-  	"github.com/julienschmidt/httprouter"
  	"net/http"
-  	"strconv"
  )
  
  func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) {
  	fmt.Fprintln(w, "create a new movie")
  }
  
  func (app *application) showMovieHandler(w http.ResponseWriter, r *http.Request) {
-  	id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
+   id, err := app.readIDParam(r)
  	if err != nil || id < 1 {
  		http.NotFound(w, r)
  		return
  	}
  	// Otherwise, interpolate the movie ID in a placeholder response.
  	fmt.Fprintf(w, "show the details of movie %d\n", id)
  }

2.3 Informacion adicional

2.3 Comportamiento personalizado de httprouter

El paquete httprouter proporciona algunas opciones de configuración que puedes utilizar para personalizar aún más el comportamiento de tu aplicación, incluyendo la posibilidad de habilitar redirecciones de barra diagonal final y habilitar la limpieza automática de rutas URL.

Mas informacion aca

Post Relacionados