DEV Community

loading...
Cover image for API Rest con Go (Golang) y PostgreSQL Parte 4

API Rest con Go (Golang) y PostgreSQL Parte 4

orlmonteverde profile image Orlando Monteverde Updated on ・16 min read

Requisitos:

  • Conocimientos básicos sobre el lenguaje go.
  • Conocimiento sobre HTTP y REST recomendable.
  • Go en su versión 1.11 o superior.
  • PostgreSQL en su versión 9.6 o superior.
  • Conocimientos básicos de SQL deseables.
  • Ganas de aprender.

Que vamos a desarrollar

Crearemos un API Rest para un microblog, estilo twitter en su versión mas básica. Con un modelo de usuarios y uno de publicaciones. Con un CRUD completo en cada caso, autenticación con JSON Web Tokens, creación, uso de middlewares y asegurar contraseña por con bcrypt. ¿Suena interesante? ¡Entonces vamos!

Meme

Indice

Código fuente hasta el momento.

El paquete response

Comencemos creando un nuevo paquete al que llamaremos response, en este agruparemos algunas funcionalidades útiles para las respuestas de nuestras funciones handler, podríamos utilizar, por ejemplo el paquete render perteneciente al ecosistema de chi, que es el paquete que estamos utilizando para manejar las rutas, o algún equivalente, pero en nuestro caso no seria necesario descargar una dependencia adicional si perfectamente podemos cubrir nuestra necesidad con unas pocas lineas de código. Dicho esto, vamos a crear nuestro nuevo paquete.

mkdir pkg/response && touch $_/response.go
Enter fullscreen mode Exit fullscreen mode

Y agregamos el siguiente contenido a nuestro paquete.

// pkg/response/response.go
package response

type ErrorMessage struct {
    Message string `json:"message"`
}
Enter fullscreen mode Exit fullscreen mode

Con este tipo, ErrorMessage podemos estandarizar las respuestas de error, solo contiene un campo Message de tipo string que representa el mensaje de error.

// pkg/response/response.go
// [...]

type Map map[string]interface{}
Enter fullscreen mode Exit fullscreen mode

Map es simplemente un map con claves de tipo string y valor de interface vacía, que, a efectos prácticos nos permite almacenar valores de cualquier tipo. Este tipo nos facilita el trabajo de serializar/deserializar, lo veremos en la practica en breve.

// pkg/response/response.go
// [...]

func JSON(w http.ResponseWriter, r *http.Request, statusCode int, data interface{}) error {
    if data == nil {
        w.Header().Set("Content-Type", "application/json; charset=utf-8")
        w.WriteHeader(statusCode)
        return nil
    }

    j, err := json.Marshal(data)
    if err != nil {
        return err
    }

    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(statusCode)
    w.Write(j)
    return nil
}
Enter fullscreen mode Exit fullscreen mode

Con la función JSON podemos realizar respuestas en formato JSON, evitando repetir constantemente la parte de serializar la data, y sinceramente lo vamos a ocupar bastante 😜. En caso de que el argumento data sea diferente de nil lo serializamos como JSON y lo escribimos en la respuesta.

// pkg/response/response.go
// [...]

func HTTPError(w http.ResponseWriter, r *http.Request, statusCode int, message string) error {
    msg := ErrorMessage{
        Message: message,
    }

    return JSON(w, r, statusCode, msg)
}
Enter fullscreen mode Exit fullscreen mode

En lo que respecta a la función HTTPError es solo una manera conveniente de crear un nuevo ErrorMessage a partir del message que recibe y luego enviarlo como respuesta JSON gracias a la función JSON que creamos antes.

Si algo de lo anterior no les ha quedado claro estoy seguro de que sera mas fácil de entender cuando lo veamos en acción, cosa que haremos de inmediato. Es momento de mi parte favorita, manejar las peticiones 😁.

Manejar las peticiones

Si recuerdan en el primer articulo creamos el paquete server (internal/server), en el que dejamos listas algunas configuraciones, ahora es momento de regresar a ese paquete y agregar nuestros handlers.

Para comenzar vamos a crear un paquete al que llamaremos v1 dentro del paquete server, es decir, sera una carpeta dentro de este ultimo. No es realmente necesario trabajar los handlers en un paquete separado de server, pero si a futuro debemos agregar una nueva versión de nuestra API sera mas conveniente tenerlo organizado de esta forma. Pero esa es solo mi opinión y no la verdad absoluta, siéntanse libres de probar, buscar y compartir cualquier otra opción o idea al respecto.

mkdir internal/server/v1
Enter fullscreen mode Exit fullscreen mode

Rutas de usuario

En primer lugar crearemos un nuevo archivo dentro del directorio v1 que creamos recientemente, a este archivo lo llamaremos user_router.go.

touch internal/server/v1/user_router.go
Enter fullscreen mode Exit fullscreen mode

Y agregamos en este nuevo archivo el siguiente contenido.

// internal/server/v1/user_router.go
package v1

import "github.com/orlmonteverde/go-postgres-microblog/pkg/user"

type UserRouter struct {
    Repository user.Repository
}
Enter fullscreen mode Exit fullscreen mode

Esta estructura que llamamos UserRouter contiene el campo Repository, que es del tipo user.Repository, este tipo es una interfaz creada en la segunda parte, cuyos métodos coinciden con el tipo UserRepository que creamos en la tercera parte. Esta no es una casualidad, se realizo intencionalmente para poder usar UserRepository como user.Repository.

Es posible que se estén preguntando que caso tiene usar la interfaz en lugar de directamente utilizar UserRepository, en nuestro caso no habría mucha diferencia, pero utilizar la interfaz brinda varias ventajas, principalmente hace fácil remplazar el Repository por cualquier tipo que satisfaga la interfaz, por ejemplo otra implementación utilizando un motor de bases de datos diferente, o un tipo falso creado solo para propósitos de testing

Crear un usuario

Comencemos por el método de crear usuario, el objetivo es recibir un usuario en formato JSON desde el cuerpo de la petición deserializarlo en una variable de tipo user.User y almacenarlo en nuestra base de datos.

// internal/server/v1/user_router.go
// […]

func (ur *UserRouter) CreateHandler(w http.ResponseWriter, r *http.Request) {
    var u user.User
    err := json.NewDecoder(r.Body).Decode(&u)
    if err != nil {
        response.HTTPError(w, r, http.StatusBadRequest, err.Error())
        return
    }

    defer r.Body.Close()

    ctx := r.Context()
    err = ur.Repository.Create(ctx, &u)
    if err != nil {
        response.HTTPError(w, r, http.StatusBadRequest, err.Error())
        return
    }

    u.Password = ""
    w.Header().Add("Location", fmt.Sprintf("%s%d", r.URL.String(), u.ID))
    response.JSON(w, r, http.StatusCreated, response.Map{"user": u})
}
Enter fullscreen mode Exit fullscreen mode

En primer lugar estamos definiendo una variable u de tipo user.User, luego usamos la función NewDecoder del paquete json, de la librería estándar para obtener un puntero json.Decoder, que usamos inmediatamente para deserializar la data dentro de la variable u, es importante que sea un puntero porque necesita acceder a su lugar en memoria y realizar cambios en su valor.

Como pueden ver, estamos utilizando la función HTTPError del paquete response que creamos en un inicio para enviar una respuesta de error en caso de que exista, este proceso lo estaremos realizando frecuentemente en los handlers.

Creamos el usuario utilizando el método Create de Repository, al cual le pasamos un context.Context, en este caso el Context del Request, y un puntero a nuestra variable u que es ese punto contiene los datos del usuario a crear. Comprobamos si existe un error, de manera similar a lo hecho anteriormente.

Cambiamos el valor de la contraseña del usuario a un string vació para que el campo sea ignorado al generar el JSON. Agregamos a la cabecera de la respuesta el campo Location dando como valor la dirección del recurso que acabamos de crear, esto ultimo no es obligatorio pero es una buena practica. Para, finalmente, enviar una respuesta de JSON, gracias a la función JSON del paquete response que creamos al inicio (les dije que seria útil 😁) con el código de estado 201 (created).

Podrán notar que como data no estamos enviando directamente el usuario sino que este es enviado como valor de un campo llamado user en el response.Map, yo acostumbro enviarlo de esa forma pero si prefieren enviar el usuario directamente es valido.

Obtener usuarios

Ahora que ya podemos crear usuario veamos como obtener los usuarios ya registrados.

// internal/server/v1/user_router.go
// […]

func (ur *UserRouter) GetAllHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    users, err := ur.Repository.GetAll(ctx)
    if err != nil {
        response.HTTPError(w, r, http.StatusNotFound, err.Error())
        return
    }

    response.JSON(w, r, http.StatusOK, response.Map{"users": users})
}
Enter fullscreen mode Exit fullscreen mode

En este caso no necesitamos información proveniente del usuario, utilizamos el método GetAll de Repository para obtener todos los usuarios, comprobamos la existencia de un error y enviamos una respuesta de tipo JSON con los usuarios obtenidos, esta vez con el código 200 (OK).

Obtener un usuario

Con el método GetAllHandler podemos obtener todos los usuarios, pero algunas veces solo necesitamos la información de un usuario, así que vamos a crear un handler que nos permita hacer eso.

// internal/server/v1/user_router.go
// […]

func (ur *UserRouter) GetOneHandler(w http.ResponseWriter, r *http.Request) {
    idStr := chi.URLParam(r, "id")

    id, err := strconv.Atoi(idStr)
    if err != nil {
        response.HTTPError(w, r, http.StatusBadRequest, err.Error())
        return
    }

    ctx := r.Context()
    u, err := ur.Repository.GetOne(ctx, uint(id))
    if err != nil {
        response.HTTPError(w, r, http.StatusNotFound, err.Error())
        return
    }

    response.JSON(w, r, http.StatusOK, response.Map{"user": u})
}
Enter fullscreen mode Exit fullscreen mode

Para obtener un usuario especifico es necesaria una manera de identificarlo, en nuestro caso la mejor opción es el id asignado por la base de datos. Entonces necesitamos obtener ese id en a petición de alguna manera, considerando que sera una petición GET y no se debería enviar información por el Body. Una manera muy común de realizar esto es por medio de Query o como parámetros en la URL, vamos utilizar este ultimo, y chi nos facilita el trabajo gracias la función URLParam con la que podemos obtener el valor de un parámetro de la URL del http.Request.

Es importante resaltar que la función URLParam retorna un string, pero nosotros necesitamos el valor como un int (como un uint realmente pero esa es una conversión sencilla 😀), cosa que podemos realizar fácilmente con la función Atoi del paquete strconv.

El resto es muy similar a lo que hicimos en el handler GetAllHandler, solo que esta vez utilizamos el método GetOne de Repository y no olviden convertir el id a uint antes de pasarlo a la función. Si todo ha marchado bien, retornamos el usuario en formato JSON.

Actualizar usuario

Ahora que ya podemos crear usuarios y obtener un usuario ya creado, vamos como actualizar un usuario existente.

// internal/server/v1/user_router.go
// […]

func (ur *UserRouter) UpdateHandler(w http.ResponseWriter, r *http.Request) {
    idStr := chi.URLParam(r, "id")

    id, err := strconv.Atoi(idStr)
    if err != nil {
        response.HTTPError(w, r, http.StatusBadRequest, err.Error())
        return
    }

    var u user.User
    err = json.NewDecoder(r.Body).Decode(&u)
    if err != nil {
        response.HTTPError(w, r, http.StatusBadRequest, err.Error())
        return
    }

    defer r.Body.Close()

    ctx := r.Context()
    err = ur.Repository.Update(ctx, uint(id), u)
    if err != nil {
        response.HTTPError(w, r, http.StatusNotFound, err.Error())
        return
    }

    response.JSON(w, r, http.StatusOK, nil)
}
Enter fullscreen mode Exit fullscreen mode

Esta combina varias cosas que vimos anteriormente en GetOneHandler como obtener el id y convertirlo a int, con cosas que vimos en CreateHandler para deserializar JSON con ayuda del paquete json.

Hecho esto, utilizamos el método Update de Repository para actualizar el usuario y si no existe algún error enviamos una respuesta con estado 200 (OK), en este caso sin data. También se podría considerar retornar el usuario actualizado, pero mantengamos esto sencillo.

Borrar un usuarios

Podemos crear, actualizar y leer usuario, eso es genial, pero aun no falta una operación para completar nuestro CRUD, y esa es borrar.

// internal/server/v1/user_router.go
// […]

func (ur *UserRouter) DeleteHandler(w http.ResponseWriter, r *http.Request) {
    idStr := chi.URLParam(r, "id")

    id, err := strconv.Atoi(idStr)
    if err != nil {
        response.HTTPError(w, r, http.StatusBadRequest, err.Error())
        return
    }

    ctx := r.Context()
    err = ur.Repository.Delete(ctx, uint(id))
    if err != nil {
        response.HTTPError(w, r, http.StatusNotFound, err.Error())
        return
    }

    response.JSON(w, r, http.StatusOK, response.Map{})
}
Enter fullscreen mode Exit fullscreen mode

Este handler es bastante sencillo y muy parecido a GetOneHandler. Solo obtenemos el id de la URL, lo convertimos a int y utilizamos el método Delete de Repository para eliminar el usuario con el correspondiente id. Finalmente enviando una respuesta JSON con código 200 (OK) y un objeto vacío para representar que se borro satisfactoriamente.

Definiendo rutas

Hasta ahora tenemos definida la lógica para manejar las peticiones pero aun no definimos que rutas deben ser manejadas por cada handler. Esto es bastante fácil con ayuda de chi y similares.

El donde y como definir las rutas puede variar, simplemente pueden ponerlo donde las parezca mas intuitivo, en este caso lo haremos de la siguiente manera:

// internal/server/v1/user_router.go
// […]

func (ur *UserRouter) Routes() http.Handler {
    r := chi.NewRouter()
    // TODO: add routes.

    return r
}
Enter fullscreen mode Exit fullscreen mode

En este nuevo método, que llamamos Routes, vamos a definir las rutas especificas para el modelo usuario. Aprovechemos para aprender un poco sobre la manera en la que manejamos las rutas con chi, comenzaremos con GetAllHandler de esta manera:

// internal/server/v1/user_router.go
// […]

func (ur *UserRouter) Routes() http.Handler {
    r := chi.NewRouter()

    r.Get("/", ur.GetAllHandler)
    return r
}
Enter fullscreen mode Exit fullscreen mode

Como pueden ver, r que es una puntero al tipo chi.Mux tiene métodos que hacen referencia a los métodos http como GET, POST, PUT, DELETE, entre otros. De esta manera es muy sencillo vincular un handler con un método http especifico.

Gracias a lo anterior, podemos vincular el método CreateHandler al mismo patrón de ruta pero con un método http diferente, en este caso POST.

// internal/server/v1/user_router.go
// […]

func (ur *UserRouter) Routes() http.Handler {
    r := chi.NewRouter()

    r.Get("/", ur.GetAllHandler)

    r.Post("/", ur.CreateHandler)

    return r
}
Enter fullscreen mode Exit fullscreen mode

De igual forma podemos agregar el resto de los handlers, pero debemos recordar que los tres restantes tomaban un parámetro por su URL y aun no hemos visto como hacer eso. Así que tomemos como ejemplo el método GetOneHandler.

// internal/server/v1/user_router.go
// […]

func (ur *UserRouter) Routes() http.Handler {
    r := chi.NewRouter()

    r.Get("/", ur.GetAllHandler)

    r.Post("/", ur.CreateHandler)

    r.Get("/{id}", ur.GetOneHandler)

    return r
}
Enter fullscreen mode Exit fullscreen mode

Como posiblemente supongan, con esas sintaxis de llaves ({}) podemos definir los parámetros en la URL, y el texto que se encuentra dentro de ellas sera el que usaremos como clave al momento de recuperar ese valor gracias a la función URLParam del paquete chi, que en este caso es id. Ahora que sabemos esto, es bastante sencillo agregar los handlers restantes con sus respectivos métodos: DELETE para DeleteHandler y PUT para UpdateHandler.

// internal/server/v1/user_router.go
// […]

func (ur *UserRouter) Routes() http.Handler {
    r := chi.NewRouter()

    r.Get("/", ur.GetAllHandler)

    r.Post("/", ur.CreateHandler)

    r.Get("/{id}", ur.GetOneHandler)

    r.Put("/{id}", ur.UpdateHandler)

    r.Delete("/{id}", ur.DeleteHandler)

    return r
}
Enter fullscreen mode Exit fullscreen mode

Rutas de publicaciones

Este proceso un bastante similar al que realizamos con los usuarios, así que avanzaremos mas rápido o resultaría muy repetitivo. Voy a compartir el código en cada caso y solo me detendré a explicar puntos que, a mi criterio, requieran mayor explicación.

Si algo no les queda claro, siéntanse libres de dirigirse (con o sin antorchas 😬) hacia la caja de comentarios y les responderé tan pronto como me resulte posible.

Iniciamos creando un nuevo archivo dentro de paquete v1 al cual llamaremos post_router.go

touch internal/server/v1/post_router.go
Enter fullscreen mode Exit fullscreen mode

Y agregamos el siguiente contenido.

// internal/server/v1/post_router.go
package v1

import (
    "github.com/orlmonteverde/go-postgres-microblog/pkg/post"
)

type PostRouter struct {
    Repository post.Repository
}
Enter fullscreen mode Exit fullscreen mode

Hecho esto, podemos comenzar a agregar los métodos correspondientes.

Crear publicación

// internal/server/v1/post_router.go
// […]

func (pr *PostRouter) CreateHandler(w http.ResponseWriter, r *http.Request) {
    var p post.Post
    err := json.NewDecoder(r.Body).Decode(&p)
    if err != nil {
        response.HTTPError(w, r, http.StatusBadRequest, err.Error())
        return
    }

    defer r.Body.Close()

    ctx := r.Context()
    err = pr.Repository.Create(ctx, &p)
    if err != nil {
        response.HTTPError(w, r, http.StatusBadRequest, err.Error())
        return
    }

    w.Header().Add("Location", fmt.Sprintf("%s%d", r.URL.String(), p.ID))
    response.JSON(w, r, http.StatusCreated, response.Map{"post": p})
}
Enter fullscreen mode Exit fullscreen mode

Obtener publicaciones

// internal/server/v1/post_router.go
// […]

func (pr *PostRouter) GetAllHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    posts, err := pr.Repository.GetAll(ctx)
    if err != nil {
        response.HTTPError(w, r, http.StatusNotFound, err.Error())
        return
    }

    response.JSON(w, r, http.StatusOK, response.Map{"posts": posts})
}
Enter fullscreen mode Exit fullscreen mode

Obtener una publicación

// internal/server/v1/post_router.go
// […]

func (pr *PostRouter) GetOneHandler(w http.ResponseWriter, r *http.Request) {
    idStr := chi.URLParam(r, "id")

    id, err := strconv.Atoi(idStr)
    if err != nil {
        response.HTTPError(w, r, http.StatusBadRequest, err.Error())
        return
    }

    ctx := r.Context()
    p, err := pr.Repository.GetOne(ctx, uint(id))
    if err != nil {
        response.HTTPError(w, r, http.StatusNotFound, err.Error())
        return
    }

    response.JSON(w, r, http.StatusOK, response.Map{"post": p})
}
Enter fullscreen mode Exit fullscreen mode

Actualizar publicación

// internal/server/v1/post_router.go
// […]

func (pr *PostRouter) UpdateHandler(w http.ResponseWriter, r *http.Request) {
    idStr := chi.URLParam(r, "id")

    id, err := strconv.Atoi(idStr)
    if err != nil {
        response.HTTPError(w, r, http.StatusBadRequest, err.Error())
        return
    }

    var p post.Post
    err = json.NewDecoder(r.Body).Decode(&p)
    if err != nil {
        response.HTTPError(w, r, http.StatusBadRequest, err.Error())
        return
    }

    defer r.Body.Close()

    ctx := r.Context()
    err = pr.Repository.Update(ctx, uint(id), p)
    if err != nil {
        response.HTTPError(w, r, http.StatusNotFound, err.Error())
        return
    }

    response.JSON(w, r, http.StatusOK, nil)
}
Enter fullscreen mode Exit fullscreen mode

Borrar publicación

// internal/server/v1/post_router.go
// […]

func (pr *PostRouter) DeleteHandler(w http.ResponseWriter, r *http.Request) {
    idStr := chi.URLParam(r, "id")

    id, err := strconv.Atoi(idStr)
    if err != nil {
        response.HTTPError(w, r, http.StatusBadRequest, err.Error())
        return
    }

    ctx := r.Context()
    err = pr.Repository.Delete(ctx, uint(id))
    if err != nil {
        response.HTTPError(w, r, http.StatusNotFound, err.Error())
        return
    }

    response.JSON(w, r, http.StatusOK, response.Map{})
}
Enter fullscreen mode Exit fullscreen mode

Obtener las publicaciones de un usuario

¿Recuerdan que comente antes que solo me detendría a explicar detalles que considerara diferentes a los ya explicados para UserRouter? Pues para que no parezca que solo estoy pegando código (eso estaba haciendo 🤫), explicare este handler un poco, aunque es bastante parecido a GetOneHandler.

// internal/server/v1/post_router.go
// […]

func (pr *PostRouter) GetByUserHandler(w http.ResponseWriter, r *http.Request) {
    userIDStr := chi.URLParam(r, "userId")

    userID, err := strconv.Atoi(userIDStr)
    if err != nil {
        response.HTTPError(w, r, http.StatusBadRequest, err.Error())
        return
    }

    ctx := r.Context()
    posts, err := pr.Repository.GetByUser(ctx, uint(userID))
    if err != nil {
        response.HTTPError(w, r, http.StatusNotFound, err.Error())
        return
    }

    response.JSON(w, r, http.StatusOK, response.Map{"posts": posts})
}
Enter fullscreen mode Exit fullscreen mode

En este caso el parámetro que estamos extrayendo de la URL no se llama id sino userId, aunque esto es un detalle menor, podríamos llamarlo de la manera en la que consideremos mas adecuada siempre y cuando usemos la misma clave al definir la ruta. Realizamos la conversión de string a int, utilizamos el método GetByUser de Repository para obtener las publicaciones relacionadas a un usuario por su id y enviamos la respuesta de tipo JSON.

Definiendo rutas

De manera similar al lo que realizamos con UserRouter, crearemos un método Routes que retorne un http.Handler dentro del cual definiremos nuestras rutas.

// internal/server/v1/post_router.go
// […]

func (pr *PostRouter) Routes() http.Handler {
    r := chi.NewRouter()

    r.Get("/user/{userId}", pr.GetByUserHandler)

    r.Get("/", pr.GetAllHandler)

    r.Post("/", pr.CreateHandler)

    r.Get("/{id}", pr.GetOneHandler)

    r.Put("/{id}", pr.UpdateHandler)

    r.Delete("/{id}", pr.DeleteHandler)

    return r
}
Enter fullscreen mode Exit fullscreen mode

Conectando las rutas

Actualmente tenemos nuestras funciones handler y rutas definidas tanto para usuarios como para publicaciones, pero aunque levantemos el servidor no podremos realizar consultas a estas rutas porque aun no las conectamos a nuestro Server. Así que crearemos un nuevo archivo dentro del paquete v1 al que llamaremos api.go.

touch internal/server/v1/api.go
Enter fullscreen mode Exit fullscreen mode

Y agregamos el siguiente contenido

// internal/server/v1/api.go
package v1

import (
    "net/http"

    "github.com/go-chi/chi"
)

func New() http.Handler {
    r := chi.NewRouter()

    return r
}
Enter fullscreen mode Exit fullscreen mode

En este punto vamos a aprovechar la posibilidad de agrupar rutas que nos proporciona chi, pero primero debemos definir una nueva variable de tipo puntero a UserRouter.

// internal/server/v1/api.go
// […]

func New() http.Handler {
    r := chi.NewRouter()

    ur := &UserRouter{
        Repository: &data.UserRepository{
            Data: data.New(),
        },
    }

    return r
}
Enter fullscreen mode Exit fullscreen mode

Nuestra nueva variable ur es un puntero a UserRouter en el que estamos inicializando su campo Repository con un puntero al tipo data.UserRepository que desarrollamos en la parte 3, el cual, a su vez, contiene un campo llamado Data que es un puntero al tipo data.Data y lo estamos inicializando gracias a la función New del paquete data que creamos en la parte 2.

// internal/server/v1/api.go
// […]

func New() http.Handler {
    r := chi.NewRouter()

    ur := &UserRouter{Repository:
        &data.UserRepository{Data: data.New()},
    }

    r.Mount("/users", ur.Routes())

    return r
}
Enter fullscreen mode Exit fullscreen mode

Con nuestra variable ur preparada podemos utilizar la el método Mount del Router podemos anidar las rutas definidas dentro del http.Handler que retorna el método Routes de nuestro UserRouter. Dicho de otra forma, las rutas que definimos dentro de Routes ahora tendrán como base /users, por ejemplo, para el caso de r.Get("/", ur.GetAllHandler), la ruta sera /users/. Quedara mas claro en un momento cuando realicemos peticiones de prueba a nuestra API.

// internal/server/v1/api.go
// […]

func New() http.Handler {
    r := chi.NewRouter()

    // [...]

    pr := &PostRouter{
        Repository: &data.PostRepository{
            Data: data.New(),
        },
    }

    r.Mount("/posts", pr.Routes())

    return r
}
Enter fullscreen mode Exit fullscreen mode

De manera similar agregamos nuestras rutas para publicaciones, en este caso bajo la ruta /posts. Hecho esto, vamos a nuestro archivo server.go en el paquete server (internal/server/server.go) y agregamos las rutas de nuestra API versión uno.

// internal/server/server.go
package server

// [...]

// New inicialize a new server with configuration.
func New(port string) (*Server, error) {
    r := chi.NewRouter()

    // API routes version 1.
    r.Mount("/api/v1", v1.New())

    serv := &http.Server{
        Addr:         ":" + port,
        Handler:      r,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    server := Server{server: serv}

    return &server, nil
}

// [...]
Enter fullscreen mode Exit fullscreen mode

Ahora ya tenemos nuestras rutas correctamente configuradas 😀. Vamos levantar el servidor y hacer pruebas.

go build ./cmd/microblog && ./microblog
Enter fullscreen mode Exit fullscreen mode

Si ahora visitamos desde nuestro navegador de confianza la dirección http://localhost:9000/api/v1/users/ deberían obtener una salida similar a la siguiente.

{"users":null}
Enter fullscreen mode Exit fullscreen mode

De igual forma si visitamos http://localhost:9000/api/v1/posts/.

{"posts":null}
Enter fullscreen mode Exit fullscreen mode

Tenia la intención de realizar las pruebas a todos los endpoints con curl y/o postman, pero esta parte se ha extendido bastante, así que lo estaremos abordando en la siguiente junto con JSON Web Tokens.

Por ahora lo dejaremos hasta aquí, en el siguiente articulo continuaremos con el desarrollo de nuestra API. Les comparto el repositorio con el código del proyecto hasta el momento.

Realmente espero que el contenido les sea de utilidad, y cualquier duda o sugerencia, por favor, déjenla en los comentarios y les responderé tan pronto como me resulte posible. Saludos y gracias por leer.

konosuba

Discussion (2)

pic
Editor guide
Collapse
fifiuro profile image
fifiuro

Un saludo Cordial Orlando:
Muy bueno el ejemplo, gracias por compartir tu conocimiento, muchas felicitaciones.
Estaré pendiente a la siguiente parte del ejemplo.

Collapse
orlmonteverde profile image
Orlando Monteverde Author

Saludos y gracias a ti por tomarte el tiempo de escribir. Me alegra que el contenido resulte de utilidad. Espero pronto poder publicar la parte 5 😅