Saltar al contenido principal

Genera una API HTTP CRUD completamente funcional en Go con Ent

· 11 min de lectura
[Traducción Beta No Oficial]

Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →

Cuando decimos que uno de los principios fundamentales de Ent es "Esquema como Código", queremos decir algo más que "el DSL de Ent para definir entidades y sus relaciones se hace con código Go normal". El enfoque único de Ent, comparado con otros ORMs, es expresar toda la lógica relacionada con una entidad, como código, directamente en la definición del esquema.

Con Ent, los desarrolladores pueden escribir toda la lógica de autorización (llamada "Privacidad" en Ent) y todos los efectos secundarios de mutaciones (llamados "Hooks" en Ent) directamente en el esquema. Tener todo en el mismo lugar es muy conveniente, pero su verdadero poder se revela cuando se combina con la generación de código.

Si los esquemas se definen así, se hace posible generar automáticamente código para servidores de producción completamente funcionales. Si trasladamos la responsabilidad de las decisiones de autorización y efectos secundarios personalizados desde la capa RPC a la capa de datos, la implementación de los endpoints CRUD básicos (Crear, Leer, Actualizar y Eliminar) se vuelve tan genérica que puede ser generada por máquinas. Esta es exactamente la idea detrás de las populares extensiones GraphQL y gRPC de Ent.

Hoy nos complace presentar una nueva extensión de Ent llamada elk que puede generar automáticamente endpoints API RESTful completamente funcionales a partir de tus esquemas Ent. elk se esfuerza por automatizar todo el trabajo tedioso de configurar los endpoints CRUD básicos para cada entidad que añadas a tu grafo, incluyendo registro de actividad, validación del cuerpo de la solicitud, carga temprana de relaciones y serialización, todo ello evitando la reflexión y manteniendo la seguridad de tipos.

¡Comencemos!

Empezando

La versión final del código a continuación puede encontrarse en GitHub.

Comienza creando un nuevo proyecto de Go:

mkdir elk-example
cd elk-example
go mod init elk-example

Invoca el generador de código de ent y crea dos esquemas: User, Pet:

go run -mod=mod entgo.io/ent/cmd/ent new Pet User

Tu proyecto debería tener ahora este aspecto:

.
├── ent
│ ├── generate.go
│ └── schema
│ ├── pet.go
│ └── user.go
├── go.mod
└── go.sum

A continuación, añade el paquete elk a nuestro proyecto:

go get -u github.com/masseelch/elk

elk utiliza la API de extensiones de Ent para integrarse con la generación de código de Ent. Esto requiere que usemos el paquete entc (ent codegen) como se describe aquí. Sigue estos tres pasos para habilitarlo y configurar Ent para trabajar con la extensión elk:

1. Crea un nuevo archivo Go llamado ent/entc.go y pega el siguiente contenido:

// +build ignore

package main

import (
"log"

"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
"github.com/masseelch/elk"
)

func main() {
ex, err := elk.NewExtension(
elk.GenerateSpec("openapi.json"),
elk.GenerateHandlers(),
)
if err != nil {
log.Fatalf("creating elk extension: %v", err)
}
err = entc.Generate("./schema", &gen.Config{}, entc.Extensions(ex))
if err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

2. Edita el archivo ent/generate.go para ejecutar el archivo ent/entc.go:

package ent

//go:generate go run -mod=mod entc.go

3. elk utiliza algunos paquetes externos en su código generado. Actualmente, debes obtener estos paquetes manualmente una vez al configurar elk:

go get github.com/mailru/easyjson github.com/masseelch/render github.com/go-chi/chi/v5 go.uber.org/zap

¡Con estos pasos completados, todo está listo para usar nuestro Ent potenciado por elk! Para aprender más sobre Ent, cómo conectarse a diferentes tipos de bases de datos, ejecutar migraciones o trabajar con entidades, visita el Tutorial de Configuración.

Generación de manejadores HTTP CRUD con elk

Para generar los manejadores HTTP completamente funcionales, primero necesitamos crear una definición de esquema Ent. Abre y edita ent/schema/pet.go:

package schema

import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)

// Pet holds the schema definition for the Pet entity.
type Pet struct {
ent.Schema
}

// Fields of the Pet.
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.Int("age"),
}
}

Hemos añadido dos campos a nuestra entidad Pet: name y age. El ent.Schema simplemente define los campos de nuestra entidad. Para generar código ejecutable a partir de nuestro esquema, ejecuta:

go generate ./...

Observa que, además de los archivos que Ent generaría normalmente, se ha creado un directorio llamado ent/http. Estos archivos fueron generados por la extensión elk y contienen el código para los manejadores HTTP generados. Por ejemplo, aquí tienes parte del código generado para una operación de lectura en la entidad Pet:

const (
PetCreate Routes = 1 << iota
PetRead
PetUpdate
PetDelete
PetList
PetRoutes = 1<<iota - 1
)

// PetHandler handles http crud operations on ent.Pet.
type PetHandler struct {
handler

client *ent.Client
log *zap.Logger
}

func NewPetHandler(c *ent.Client, l *zap.Logger) *PetHandler {
return &PetHandler{
client: c,
log: l.With(zap.String("handler", "PetHandler")),
}
}

// Read fetches the ent.Pet identified by a given url-parameter from the
// database and renders it to the client.
func (h *PetHandler) Read(w http.ResponseWriter, r *http.Request) {
l := h.log.With(zap.String("method", "Read"))
// ID is URL parameter.
id, err := strconv.Atoi(chi.URLParam(r, "id"))
if err != nil {
l.Error("error getting id from url parameter", zap.String("id", chi.URLParam(r, "id")), zap.Error(err))
render.BadRequest(w, r, "id must be an integer greater zero")
return
}
// Create the query to fetch the Pet
q := h.client.Pet.Query().Where(pet.ID(id))
e, err := q.Only(r.Context())
if err != nil {
switch {
case ent.IsNotFound(err):
msg := stripEntError(err)
l.Info(msg, zap.Error(err), zap.Int("id", id))
render.NotFound(w, r, msg)
case ent.IsNotSingular(err):
msg := stripEntError(err)
l.Error(msg, zap.Error(err), zap.Int("id", id))
render.BadRequest(w, r, msg)
default:
l.Error("could not read pet", zap.Error(err), zap.Int("id", id))
render.InternalServerError(w, r, nil)
}
return
}
l.Info("pet rendered", zap.Int("id", id))
easyjson.MarshalToHTTPResponseWriter(NewPet2657988899View(e), w)
}

A continuación, veamos cómo crear un servidor HTTP RESTful real que pueda gestionar tus entidades Pet. Crea un archivo llamado main.go y añade el siguiente contenido:

package main

import (
"context"
"fmt"
"log"
"net/http"

"elk-example/ent"
elk "elk-example/ent/http"

"github.com/go-chi/chi/v5"
_ "github.com/mattn/go-sqlite3"
"go.uber.org/zap"
)

func main() {
// Create the ent client.
c, err := ent.Open("sqlite3", "./ent.db?_fk=1")
if err != nil {
log.Fatalf("failed opening connection to sqlite: %v", err)
}
defer c.Close()
// Run the auto migration tool.
if err := c.Schema.Create(context.Background()); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
// Router and Logger.
r, l := chi.NewRouter(), zap.NewExample()
// Create the pet handler.
r.Route("/pets", func(r chi.Router) {
elk.NewPetHandler(c, l).Mount(r, elk.PetRoutes)
})
// Start listen to incoming requests.
fmt.Println("Server running")
defer fmt.Println("Server stopped")
if err := http.ListenAndServe(":8080", r); err != nil {
log.Fatal(err)
}
}

Luego, inicia el servidor:

go run -mod=mod main.go

¡Enhorabuena! Ahora tenemos un servidor en funcionamiento que sirve la API de Pets. Podríamos pedir al servidor una lista de todas las mascotas en la base de datos, pero aún no hay ninguna. Primero creemos una:

curl -X 'POST' -H 'Content-Type: application/json' -d '{"name":"Kuro","age":3}' 'localhost:8080/pets'

Deberías obtener esta respuesta:

{
"age": 3,
"id": 1,
"name": "Kuro"
}

Si miras en la terminal donde se está ejecutando el servidor, también puedes ver el registro integrado de elk:

{
"level": "info",
"msg": "pet rendered",
"handler": "PetHandler",
"method": "Create",
"id": 1
}

elk utiliza zap para el registro. Para saber más sobre él, consulta su documentación.

Relaciones

Para ilustrar más características de elk, ampliemos nuestro gráfico. Edita ent/schema/user.go y ent/schema/pet.go:

ent/schema/pet.go
// Edges of the Pet.
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("pets").
Unique(),
}
}

ent/schema/user.go
package schema

import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)

// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}

// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.Int("age"),
}
}

// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type),
}
}

Hemos creado una relación Uno-a-Muchos entre los esquemas Pet y User: una mascota pertenece a un usuario, y un usuario puede tener varias mascotas.

Vuelve a ejecutar el generador de código:

go generate ./...

No olvides registrar el UserHandler en nuestro enrutador. Añade las siguientes líneas a main.go:

[...]
r.Route("/pets", func(r chi.Router) {
elk.NewPetHandler(c, l, v).Mount(r, elk.PetRoutes)
})
+ // Create the user handler.
+ r.Route("/users", func(r chi.Router) {
+ elk.NewUserHandler(c, l, v).Mount(r, elk.UserRoutes)
+ })
// Start listen to incoming requests.
fmt.Println("Server running")
[...]

Después de reiniciar el servidor, podemos crear un User que sea dueño de la mascota Kuro que creamos anteriormente:

curl -X 'POST' -H 'Content-Type: application/json' -d '{"name":"Elk","age":30,"owner":1}' 'localhost:8080/users'

El servidor devuelve la siguiente respuesta:

{
"age": 30,
"edges": {},
"id": 1,
"name": "Elk"
}

En la salida podemos ver que el usuario se ha creado, pero las aristas están vacías. Por defecto, elk no incluye las aristas en su salida. Puedes configurar elk para representar las aristas usando una característica llamada "grupos de serialización". Anota tus esquemas con las estructuras elk.SchemaAnnotation y elk.Annotation. Edita ent/schema/user.go y añádelas:

// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("pets", Pet.Type).
Annotations(elk.Groups("user")),
}
}

// Annotations of the User.
func (User) Annotations() []schema.Annotation {
return []schema.Annotation{elk.ReadGroups("user")}
}

Las elk.Annotation añadidas a los campos y aristas indican a elk que los cargue de forma ansiosa y los añada a la carga útil si se solicita el grupo "user". La elk.SchemaAnnotation se utiliza para que la operación de lectura del UserHandler solicite "user". Ten en cuenta que cualquier campo que no tenga un grupo de serialización asociado se incluye por defecto. Las aristas, sin embargo, se excluyen a menos que se configure lo contrario.

A continuación, regenera el código una vez más y reinicia el servidor. Ahora deberías ver las mascotas de un usuario representadas si lees un recurso:

curl 'localhost:8080/users/1'
{
"age": 30,
"edges": {
"pets": [
{
"id": 1,
"name": "Kuro",
"age": 3,
"edges": {}
}
]
},
"id": 1,
"name": "Elk"
}

Validación de solicitudes

Nuestros esquemas actuales permiten establecer una edad negativa para mascotas o usuarios y podemos crear mascotas sin dueño (como hicimos con Kuro). Ent tiene soporte integrado para validaciones básicas. En algunos casos, es posible que desees validar las solicitudes realizadas a tu API antes de pasar sus datos a Ent. elk utiliza este paquete para definir reglas de validación y validar datos. Podemos crear reglas de validación separadas para operaciones de Creación y Actualización usando elk.Annotation. En nuestro ejemplo, supongamos que queremos que nuestro esquema Pet solo permita edades mayores que cero y prohiba crear una mascota sin dueño. Edita ent/schema/pet.go:

// Fields of the Pet.
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.Int("age").
Positive().
Annotations(
elk.CreateValidation("required,gt=0"),
elk.UpdateValidation("gt=0"),
),
}
}

// Edges of the Pet.
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("pets").
Unique().
Required().
Annotations(elk.Validation("required")),
}
}

A continuación, regenera el código y reinicia el servidor. Para probar nuestras nuevas reglas de validación, intentemos crear una mascota con edad inválida y sin dueño:

curl -X 'POST' -H 'Content-Type: application/json' -d '{"name":"Bob","age":-2}' 'localhost:8080/pets'

elk devuelve una respuesta detallada que incluye información sobre qué validaciones fallaron:

{
"code": 400,
"status": "Bad Request",
"errors": {
"Age": "This value failed validation on 'gt:0'.",
"Owner": "This value is required."
}
}

Observa los nombres de campos en mayúsculas. El paquete de validación usa el nombre del campo en las estructuras para generar sus errores de validación, pero puedes sobrescribirlo fácilmente, como se muestra en el ejemplo.

Si no defines reglas de validación, elk no incluirá el código de validación en su salida generada. La validación de solicitudes de elk es especialmente útil si necesitas realizar validación entre campos.

Próximas características

Esperamos que estés de acuerdo en que elk ya tiene características útiles, pero aún quedan muchas cosas emocionantes por venir. La próxima versión de elk incluirá:

  • Frontend de Flutter completamente funcional para administrar tus nodos

  • Integración de la validación de Ent en el validador actual de solicitudes

  • Más formatos de transporte (actualmente solo JSON)

Conclusión

Esta publicación muestra solo una pequeña parte de lo que elk puede hacer. Para ver más ejemplos de sus capacidades, visita el README del proyecto en GitHub. Espero que con Ent potenciado por elk, tú y tus compañeros desarrolladores puedan automatizar tareas repetitivas en la construcción de APIs RESTful y centrarse en trabajos más significativos.

elk está en una etapa temprana de desarrollo, agradecemos cualquier sugerencia o feedback. Si estás dispuesto a ayudar, nos encantaría. Los GitHub Issues son un lugar excelente para solicitar ayuda, dar feedback, sugerir ideas o contribuir.

Sobre el autor

MasseElch es ingeniero de software del ventoso y llano norte de Alemania. Cuando no está haciendo senderismo con su perro Kuro (quien tiene su propio canal de Instagram 😱) o jugando al escondite con su hijo, toma café y disfruta programando.