Skip to content

Simple Go web framework that uses generics for safer data sharing between middleware, router, and handlers

License

Notifications You must be signed in to change notification settings

BlakeWilliams/fernet

Repository files navigation

Fernet

A simple Go framework for building web applications. Fernet uses generics to provide convenient and type-safe APIs for your Handlers and Middleware.

Getting Started

package main

import (
    "fmt"
    "net/http"

    "github.com/blakewilliams/fernet"
)

// RequestContext is used to store data that is shared between middleware and
// handlers. Methods can be defined on this type to provide application specific
// functionality like rendering.
type RequestContext struct {
    currentUser *User
    fernet.RequestContext
}

// Implement a basic render string helper function.
func (r *RequestContext) RenderString(code int, s string) {
    r.ResponseWriter().WriteHeader(code)
    r.ResponseWriter().Write([]byte(s))
}

func main() {
    app := fernet.New(func(r *fernet.RequestContext) *RequestContext {
        return &RequestContext{RequestContext: r}
    })

    // Use is used to add fernet based middleware to the application.
    app.Use(func(ctx context.Context, r *RequestContext, next fernet.Handler[RequestContext]) {
        // Do something before the request is handled.
        next(ctx, r)
        // Do something after the request is handled.
    })

    // Fernet routing uses : to define named parameters in the path. Wildcards are also supported via *.
    app.Get("/hello/:name", func(ctx context.Context, r *RequestContext) {
        r.WriteString(http.StatusOK, fmt.Sprintf("Hello %s", rc.Params()["name"]))
    })

    // Handle 404s by defining a catch-all route.
    app.Get("*", func(ctx context.Context, r *RequestContext) {
        r.WriteString(http.StatusNotFound, "Not Found")
    }

    app.ListenAndServe(":3200")
}

Controllers

Controllers allow you to group related handlers together using a struct that is initialized each request and passed to the handler as an additional argument. This allows you to extract data from the request and pass it to the handler after common behavior like fetching records and authorization. e.g.

// Define a type that implements FromRequest and can store the team record.
type TeamData struct { Team *Team }

// Implement the FromRequest method. If it returns false, the handler will not
// be called. If it returns true, the request will be processed as normal.
func (td *TeamData) FromRequest(ctx context.Context, rc *AppRequestContext) error {
    td.Team = rc.TeamRepository.Find(ctx, rc.Params["team_id"])
    // Handle missing data
    if td.Team == nil {
        rc.Render404()
        return false
    }

    // Handle authorization
    if rc.TeamRepository.IsMember(ctx, rc.CurrentUser, td.Team) {
        rc.Render403()
        return false
    }

    return true
}

// Define a handler that accepts the TeamData type.
func Show(ctx context.Context, rc *AppRequestContext, td *TeamData) {
    rc.RenderJSON(http.StatusOK, td.Team)
}

// Setup the router
router := fernet.New(func(r *fernet.RequestContext) *AppRequestContext {
    return &AppRequestContext{RequestContext: r}
})

teamsController := fernet.Controller(router, *TeamData{})
teamsController.Get("/teams/:team_id", Show)

adminTeamController := teamsRouter.Namespace("/admin")
adminTeamController.Use(func(ctx context.Context, rc *AppRequestContext, next fernet.Handler[AppRequestContext]) {
    if rc.CurrentUser.Role != "admin" {
        rc.Render403()
        return
    }

    next(ctx, rc)
})
adminTeamController.Get("/teams/:team_id/settings", Update)

Groups and Namespaces

Groups are used to group routes together and apply middleware common only to those groups and subgroups. Namespaces are exactly like groups, but accept a prefix string that is prepended to all routes within the namespace.

type RequestContext struct {
    currentUser *User
    fernet.RequestContext
}

func (r *RequestContext) RenderString(code int, s string) {
    r.ResponseWriter().WriteHeader(code)
    r.ResponseWriter().Write([]byte(s))
}

app := fernet.New(func(r *fernet.RequestContext) *RequestContext {
    return &RequestContext{RequestContext: r}
})

authGroup := router.Group()
authGroup.Use(func(ctx context.Context, r *RequestContext, next fernet.Handler[RequestContext]) {
    if r.AppData.currentUser == nil {
        r.RenderString(http.StatusUnauthorized, "Unauthorized")
        return
    }

    next(ctx, r)
})

adminGroup := authGroup.Namespace("/admin")
adminGroup.Use(func(ctx context.Context, r *RequestContext, next fernet.Handler[RequestContext]) {
    if r.AppData.currentUser == nil || r.AppData.currentUser.Role != "admin" {
        r.RenderString(http.StatusUnauthorized, "Unauthorized")
        return
    }

    next(ctx, r)
})

Middleware

Fernet provides a few middleware functions out of the box. Import the github.com/blakewilliams/fernet/middleware package to use them.

  • middleware.ErrorHandler - rescues panics and calls a Handler[T] to handle the error.
  • middleware.Logger - logs requests and responses using slog.

Metal

Fernet provides Metal handlers, which operate against net/http Request and ResponseWriter types. These handlers are useful for integrating with existing middleware and libraries or modifying the request/response before it is passed to the RequestContext handler.

  • metal.MethodRewrite - Rewrites the HTTP method based on the value of the _method form value.

About

Simple Go web framework that uses generics for safer data sharing between middleware, router, and handlers

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages