Skip to content

Framework Integration

devituz edited this page Jun 3, 2026 · 1 revision

Framework integration

lagodev does not require its web framework. It plugs into whatever HTTP / RPC framework you already use. The integration shape is always the same:

your handler  →  *Service struct  →  *database.Connection  →  database

The service returns plain Go values + errors. The handler does the framework-specific marshalling. That's it — no global state, no required middleware, no hidden import cycles.

When should you use the bundled web framework? When you don't have one already and want the secure-by-default stack out of the box. When shouldn't you? When your team has invested in Gin / Fiber / Echo / Chi and switching frameworks isn't on the table. Either way, the ORM is the same.

Generate the layout

lago make:model Post -mfsc --fields="title:string,body:text,published:bool:default(false)"

You get:

models/post.go              // type Post struct { orm.Model; … }
migrations/<ts>_*.go        // schema.Create("posts", …)
factories/post_factory.go   // factory.New[Post]
seeders/post_seeder.go      // optional fixture data
services/post_service.go    // List/Get/Create/Update/Delete with context+error
controllers/post_controller.go  // *web.Context handlers wrapping the service

Below we wire the same service into seven different surfaces. Notice the service file never moves — only the handler changes per framework.

Gin

import "github.com/gin-gonic/gin"

svc := services.NewPostService(conn)
r := gin.Default()

r.GET("/posts", func(c *gin.Context) {
    posts, err := svc.List(c.Request.Context())
    if err != nil { c.JSON(500, gin.H{"error": err.Error()}); return }
    c.JSON(200, posts)
})

r.POST("/posts", func(c *gin.Context) {
    var p models.Post
    if err := c.ShouldBindJSON(&p); err != nil { c.JSON(400, gin.H{"error": err.Error()}); return }
    if err := svc.Create(c.Request.Context(), &p); err != nil { c.JSON(500, gin.H{"error": err.Error()}); return }
    c.JSON(201, p)
})

r.Run(":8080")

Full runnable example: examples/gin/. For a more Laravel-style Gin integration with Resource()-like shortcuts, see the adapters/gin module.

Fiber

import "github.com/gofiber/fiber/v2"

app := fiber.New()
app.Get("/posts", func(c *fiber.Ctx) error {
    posts, err := svc.List(c.Context())
    if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) }
    return c.JSON(posts)
})
app.Listen(":8080")

Full example: examples/fiber/.

Echo

import "github.com/labstack/echo/v4"

e := echo.New()
e.GET("/posts", func(c echo.Context) error {
    posts, err := svc.List(c.Request().Context())
    if err != nil { return c.JSON(500, map[string]string{"error": err.Error()}) }
    return c.JSON(200, posts)
})
e.Start(":8080")

Full example: examples/echo/.

Chi

import "github.com/go-chi/chi/v5"

r := chi.NewRouter()
r.Get("/posts", func(w http.ResponseWriter, r *http.Request) {
    posts, err := svc.List(r.Context())
    if err != nil { writeErr(w, 500, err); return }
    writeJSON(w, 200, posts)
})
http.ListenAndServe(":8080", r)

Full example: examples/chi/.

Plain net/http (Go 1.22+)

mux := http.NewServeMux()
mux.HandleFunc("GET /posts", func(w http.ResponseWriter, r *http.Request) {
    posts, err := svc.List(r.Context())
    // ...
})
mux.HandleFunc("GET /posts/{id}", postsCtrl.Show)
http.ListenAndServe(":8080", mux)

The generated controllers already use *web.Context signatures, but the underlying service is plain Go — point a net/http handler at the service directly and you have a working endpoint.

gRPC

type PostServer struct {
    pb.UnimplementedPostServiceServer
    svc *services.PostService
}

func (s *PostServer) ListPosts(ctx context.Context, _ *pb.ListPostsRequest) (*pb.ListPostsResponse, error) {
    items, err := s.svc.List(ctx)
    if err != nil { return nil, err }
    out := &pb.ListPostsResponse{Posts: make([]*pb.Post, len(items))}
    for i, p := range items { out.Posts[i] = toProto(&p) }
    return out, nil
}

Full example: examples/grpc/.

GraphQL (gqlgen / 99designs)

type Resolver struct { Posts *services.PostService }

func (r *queryResolver) Posts(ctx context.Context) ([]*model.Post, error) {
    items, err := r.Posts.List(ctx)
    if err != nil { return nil, err }
    out := make([]*model.Post, len(items))
    for i := range items { out[i] = toGQL(&items[i]) }
    return out, nil
}

Background jobs / cron / queue workers

The service is just as happy in a long-running goroutine:

for job := range queue {
    if err := svc.Process(ctx, job); err != nil { log.Println(err) }
}

See examples/microservice/ for a locking-aware queue worker.

When you really do need framework-specific code in the service

You don't. If a request needs file uploads, multipart, or pagination metadata, push the framework-specific decoding into the handler and call the service with the parsed values:

// handler (Gin):
r.POST("/upload", func(c *gin.Context) {
    file, _ := c.FormFile("avatar")
    open, _ := file.Open()
    defer open.Close()
    err := svc.UploadAvatar(c.Request.Context(), userID, open, file.Size)
    if err != nil { c.JSON(500, gin.H{"error": err.Error()}); return }
    c.Status(204)
})

// service (UploadAvatar takes io.Reader + size — no Gin types):
func (s *UserService) UploadAvatar(ctx context.Context, id uint64, r io.Reader, size int64) error { ... }

That's the discipline that keeps your business logic portable — you can swap Gin for Echo in an afternoon without touching services/.

See also

  • Web-Framework — the bundled web framework.
  • ORM — the building block every service shares.
  • Architecture — why the service-as-portability-seam pattern works cleanly with lagodev's executor abstraction.

Clone this wiki locally