gofiber / fiber

⚡️ Express inspired web framework written in Go
https://gofiber.io
MIT License
34.04k stars 1.67k forks source link

📝 [Proposal]: Support for Application State Management #3077

Open gaby opened 4 months ago

gaby commented 4 months ago

Feature Proposal Description

This proposal aims to introduce Application State Management feature in Fiber to enable the sharing of stateful data across middleware and request handlers efficiently. This is a feature supported in other language frameworks for example: Starlette and FastAPI.

Sources:

Alignment with Express API

In fiber these proposal is similar to ctx.Locals but applies to the whole application. Resources stored in the state can be accessed by multiple handlers, middlewares, etc.

In Express.js it's similar to app.locals, but in express the values can only be used when rendering templates.

HTTP RFC Standards Compliance

N/a

API Stability

API Methods

We could also add specific Get functions for specific common types: int, string, float, etc. For example, for getting a string from the app.State:

func (s *State) GetString(key string) (string, bool) {
    value, exists := s.state[key]
    if !exists {
        return "", false
    }
    str, ok := value.(string)
    return str, ok
}

It would be the responsibility of the developer to call to appropriate function.

Feature Examples

func main() {
    app := fiber.New()

    // Create and set the database client
    connString := "postgres://username:password@localhost:5432/database_name"
    conn, _ := pgx.Connect(context.Background(), connString)
    defer conn.Close(context.Background())

    // Store DB client in App State
    app.State.Set("db", conn)

    app.Get("/", func(c *fiber.Ctx) error {
        // Access the database connection from the AppState
        db, exists := app.State.Get("db")
        if !exists {
            return c.Status(fiber.StatusInternalServerError).SendString("Database connection not found")
        }

        // Use the database connection
        pgxConn := db.(*pgx.Conn)
        var greeting string
        err := pgxConn.QueryRow(context.Background(), "SELECT 'Hello, world!'").Scan(&greeting)
        if err != nil {
            return c.Status(fiber.StatusInternalServerError).SendString("Failed to execute query")
        }
        return c.SendString(greeting)
    })

    app.Listen(":3000")
}

Checklist:

brunodmartins commented 4 months ago

On the applications I currently use Fiber in production, we create a whole set of application components using a single pointer of structs, and perform the Dependency Injection at the bootstrap of the application to provide this "Application State". I will give an example on how we perform it today:

func main() {
    app := fiber.New()
    service := UserService{}
    handler := UserHandler{
        service: service,
    }
    app.Get("/users/:id", handler.GetUser)
    app.Listen(":3000")
}

type User struct{}

type UserService struct {
}

func (*UserService) GetUser(id string) (*User, error) {
    return nil, nil
}

type UserHandler struct{
    service UserService
}

func (handler *UserHandler) GetUser(ctx fiber.Ctx) error {
    _, _ = handler.service.GetUser(ctx.Get(":id"))
    return nil
}

In any case, such solution (Fiber's Application State) could help us improve the solution we have today.

mdelapenya commented 2 months ago

Hi from the Testcontainers for Go community 👋 ! Core maintainer here 🙋

We have a workshop for learning the fundamentals of Testcontainers in the Go language, and we have what we call local development mode, in which we could use the build to start the runtime dependencies (databases, queues, cloud services...) selectively and only when certain build tags (e.g. -tags=dev) are passed to the Go toolchain. Then, the dependencies in those "protected-by-build-tags" file will be only added to the final binary if and only if the dev mode build tags are used, not ending up in the production binary.

I think this approach could be combined with the built-in capabilities in GoFiber, to start these dependencies in a hook only when the build adds the right build tags.

You can check it here: https://github.com/testcontainers/workshop-go/blob/main/step-4-dev-mode-with-testcontainers.md

What are your thoughts on this?

gaby commented 2 months ago

@mdelapenya Does this require adding testcontainers as a dependency in Fiber go.mod? Or is there a way to keep it separate.

mdelapenya commented 2 months ago

I'm afraid I'm not an expert in the Fiber ecosistem yet, but I'd say that, if we are able to create Go modules for them so that only those using the "hooks" (or whatever implementation we use) will receive the dependencies. In the end, they will be dev-time dependencies, so the production build won't receive them.

mdelapenya commented 2 months ago

@gaby I'd expect Fiber providing a "RuntimeDependency" interface with at least the Start and Stop methods, so that this hook could control the lifecycle of those startable dependencies. An implementer of that interface (e.g. a hook for testcontainers-go module) would contribute runtime dependencies (databases, queues, cloud services...) to some state in the framework, so that the framework can handle their lifecycles (start & stop when needed).

Something like:

package fiber

import "context"

type RuntimeDependency interface {
    Start(context.Context) error
    Terminate(context.Context) error
}

// hasDependencies Checks if there are any dependency for the current application.
func (app *App) hasDependencies() bool {
    return len(app.runtimeDependencies) > 0
}

// startDependencies Handles the startup process of dependencies for the current application.
// Iterates over all dependencies and starts them, panics if any error occurs.
func (app *App) startDependencies() {
    if app.hasDependencies() {
        for _, dep := range app.runtimeDependencies {
            err := dep.Start(app.newCtx().Context())
            if err != nil {
                panic(err)
            }
        }
    }
}

// shutdownDependencies Handles the shutdown process of dependencies for the current application.
// Iterates over all dependencies and terminates them, panics if any error occurs.
func (app *App) shutdownDependencies() {
    if app.hasDependencies() {
        for _, dep := range app.runtimeDependencies {
            err := dep.Terminate(app.newCtx().Context())
            if err != nil {
                panic(err)
            }
        }
    }
}

And in app.go:

// App denotes the Fiber application.
type App struct {
    ...
    // runtimeDependencies is a list of dependencies that are used by the app (e.g. databases, caches, etc.)
    runtimeDependencies []RuntimeDependency
}

Calling app.shutdownDependencies() and app.startDependencies() where it better fits.

Quick and dirty idea, as I'm talking without knowing any about the fiber codebase yet, so please correct me if this is nonsense 😅

gaby commented 2 months ago

@mdelapenya Yeah that approach makes sense, I will add it to the idea pool

wangjq4214 commented 1 week ago

On the applications I currently use Fiber in production, we create a whole set of application components using a single pointer of structs, and perform the Dependency Injection at the bootstrap of the application to provide this "Application State". I will give an example on how we perform it today:

func main() {
  app := fiber.New()
  service := UserService{}
  handler := UserHandler{
      service: service,
  }
  app.Get("/users/:id", handler.GetUser)
  app.Listen(":3000")
}

type User struct{}

type UserService struct {
}

func (*UserService) GetUser(id string) (*User, error) {
  return nil, nil
}

type UserHandler struct{
  service UserService
}

func (handler *UserHandler) GetUser(ctx fiber.Ctx) error {
  _, _ = handler.service.GetUser(ctx.Get(":id"))
  return nil
}

In any case, such solution (Fiber's Application State) could help us improve the solution we have today.

I think it is enough. An app-level state manager may not display the benefit. But now I commit to a rust web server, axum provide a similar impl, but it enforces the handler is a stateless function