-
Notifications
You must be signed in to change notification settings - Fork 0
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
webframework? 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.
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.
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.
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/.
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/.
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/.
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.
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/.
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
}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.
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/.
-
Web-Framework — the bundled
webframework. - ORM — the building block every service shares.
- Architecture — why the service-as-portability-seam pattern works cleanly with lagodev's executor abstraction.
lagodev on GitHub · MIT-licensed — see LICENSE.