trails

module
v0.3.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 3, 2021 License: MIT

README ΒΆ

Go on the Trails

MIT license Go Reference

What's Trails

Trails unifies the patterns and solutions XY Planning Network developed to power a handful of web applications. We at XYPN prefer the slower method of walking the trails and staying closer to the dirt over something speedier on the road. Nevertheless, Trails has opinions and removes boilerplate when it can. Trails will be in v0 for the foreseeable future.

Trails provides libraries for quickly building web applications that have standard, well-defined web application needs, such as managing user sessions or routing based on user authorization. It defines the concepts needed for solving those problems through interfaces and provides default implementations of those so development can begin immediately.

So, what's in here?

http/

Trails has a web server powered by a router, middleware stack, HTML template rendering, user session management and a high-level API for crafting HTTP responses. Let's get this setup!

http/router

The first thing Trails does is initialize an HTTP router:

package main

import (
        "net/http"

        "github.com/xy-planning-network/trails/http/router"
)

func main() {
        r := router.NewRouter("DEVELOPMENT")
        r.Handle(router.Path{Path: "/", Method: http.MethodGet, Handler: getRoot}) // this and other functions in other examples would be defined elsewhere πŸ˜…
        http.ListenAndServe(":3000", r)
}

Not too useful, yet, just one route at / to direct requests to. But, this shows Trails' router implements http.Handler! We don't want to stray too far away from the standard library.

Let's get a few more routes in there.

Trails' router encourages registering routes in logically similar groups.

package main

import (
        "net/http"

        "github.com/xy-planning-network/trails/http/router"
)

func main() {
        r := router.NewRouter("DEVELOPMENT")
        base := []router.Route{
                {Path: "/login", Method: http.MethodGet, Handler: getLogin},
                {Path: "/logoff", Method: http.MethodGet, Handler: getLogoff},
                {Path: "/password/reset", Method: http.MethodGet, Handler: getPasswordReset},
                {Path: "/password/reset", Method: http.MethodPost, Handler: resetPassword},
        }
        r.HandleRoutes(base)

        http.ListenAndServe(":3000", r)
}

πŸŽ‰ We did it! Our Trails app serves up 4 distinct routes. πŸŽ‰

It is often the case that many routes for a web server share identical middleware stacks, which aid in directing, redirecting, or adding contextual information to a request. It is also often the case that small errors can lead to registering a route incorrectly, thereby unintentionally exposing a resource or not collecting data necessary for actually handling a request.

The example above does not utilize any middleware, which can be quickly rectified by using Trails' middleware library:

package main

import (
        "github.com/xy-planning-network/trails/http/middleware"
        "github.com/xy-planning-network/trails/http/router"
)

func main() {
        r := router.NewRouter("DEVELOPMENT")
        r.OnEveryRequest(middleware.InjectIPAddress())

        policies := []router.Route{
                {Path: "/terms", Method: http.MethodGet, Handler: getTerms},
                {Path: "/privacy-policy", Method: http.MethodGet, Handler: getPrivacyPolicy},
        }
        r.HandleRoutes(policies)

        base := []router.Route{
                {Path: "/login", Method: http.MethodGet, Handler: getLogin},
                {Path: "/logoff", Method: http.MethodGet, Handler: getLogoff},
                {Path: "/password/reset", Method: http.MethodGet, Handler: getPasswordReset},
                {Path: "/password/reset", Method: http.MethodPost, Handler: resetPassword},
        }
        r.HandleRoutes(
                base,
                middleware.LogRequest(logger.DefaultLogger()),
        )
}

We've added middlewares in two places in two different ways.

First, we use Router.OnEveryRequest to set a middleware that grabs the originating request's IP address on every single Route. Next, we include a middleware that logs the request when we also register or base routes. This logger will run only when a request matches one of those base routes.

Let's start getting fancy 🍸.

In our Trails app, we don't want our users who've already logged in to access neither the login page or password reset page - they should only be able to reset their password from a settings page. Furthermore, only authenticated users should be able to access the logoff endpoint. We can use Trails baked-in support for authentication to reorganize our routing:

package main

import (
        "github.com/xy-planning-network/trails/http/middleware"
        "github.com/xy-planning-network/trails/http/router"
)

func main() {
        env := "DEVELOPMENT"

        sessionstore := session.NewStoreService(env, "ABCD", "ABCD") // Read more about me in http/session

        r := router.NewRouter(env)
        r.OnEveryRequest(
                middleware.InjectIPAddress(),
                middleware.InjectSession(sessionstore, πŸ—), // πŸ—: read more about managing keys used for a *http.Request.Context in http/ctx
        )

        policies := []router.Route{
                {Path: "/terms", Method: http.MethodGet, Handler: getTerms},
                {Path: "/privacy-policy", Method: http.MethodGet, Handler: getPrivacyPolicy},
        }
        r.HandleRoutes(policies)

        unauthed := []router.Route{
                {Path: "/login", Method: http.MethodGet, Handler: getLogin},
                {Path: "/password/reset", Method: http.MethodGet, Handler: getPasswordReset},
                {Path: "/password/reset", Method: http.MethodPost, Handler: resetPassword},
        }
        r.UnauthedRoutes(πŸ—, unauthed)

        authed := []router.Route{
                {Path: "/logoff", Method: http.MethodGet, Handler: getLogoff},
                {Path: "/settings", Method: http.MethodGet, Handler: getSettings},
                {Path: "/settings", Method: http.MethodPut, Handler: updateSettings},
        }
        r.AuthedRoutes(πŸ—, "/login", "/logoff", authed)
}

Organizing routes around middleware stacks, especially those relating to authentication and authorization, can aid in eliminating subtle bugs.

http/resp

Given the Router directed a request correctly, Trails provides a high-level API for crafting responses in an HTTP handler. An HTTP handler uses a Responder to join together application-wide configuration and handler-specific needs. This standardizes responses across the web app enabling clients to rely on the HTTP headers, status codes, data schemas, etc. coming from Trails. We initialize a Responder using functional options and make that available to all our handlers:

package main

import (
	"embed"
	"net/http"

        "github.com/xy-planning-network/trails/http/resp"
	"github.com/xy-planning-network/trails/http/template"
)

//go:embed *.tmpl
var files embed.FS

type handler struct {
        *resp.Responder
}

func (h *handler) getLogin(w http.ResponseWriter, r *http.Request) {
        h.Html(w, r, resp.Tmpl("root.tmpl"))
}

func main() {
        p := template.NewParser(files) // Read more about me in http/template
        d := resp.NewResponder(resp.WithParser(p))
        h := &handler{d}
        r := router.NewRouter("DEVELOPMENT")
        r.Handle(router.Route{Path: "/", Method: http.MethodGet, Handler: r.getLogin})
}

Let's elide over the use of embed and trails/http/template for now in order to focus on this line in our handler:

h.Html(w, r, resp.Tmpl("root.tmpl"))

With the *resp.Responder embedded in our handler, we can utilize it's Html method to render HTML templates and respond with that data. Using a resp.Fn, we set the template to render. If that template needs some additional values, we can provide those with resp.Data:

func (h *handler) getLogin(w http.ResponseWriter, r *http.Request) {
        hello := map[string]interface{}{"welcomeMsg": "Hello, World!"}
        err := h.Html(w, r, resp.Tmpl("root.tmpl"), resp.Data(hello))
        if err != nil {
                h.Err(w, r, err)
                return
        }
}

These resp.Fn functional options are highly flexible. Some are generic - such as resp.Code. Some compose together multiple options - such as resp.GenericErr. Even more, some are specialized - such as resp.Props - for apps leveraging the full suite of features available in Trails.

Notably, a Responder concludes the lifecycle of an HTTP request by writing a response in one of these ways:

  • Err
    • *Responder.Err provides a backstop for malformed calls to *Responder methods by wrapping std lib's http.Error.
  • Html
    • *Responder.Html renders templates written in Go's html/template syntax.
  • Json
    • *Responder.Json renders data in JSON format.
  • Redirect
    • *Responder.Redirect redirects a request to another endpoint, a wrapper around http.Redirect.

Trees

Trails integrates with XYPN's open-source Vue component library, Trees, in two quick steps. Setup is as simple as defining the path to your base Vue template, passing in the path to your base Vue template using resp.WithVueTemplate, and include that template with resp.Vue when using resp.(*Responder).Html.

What needs to be done?

  • Database connection
  • Database migrations
  • Routing
  • Middlewares
  • Response handling
  • Session management
  • Form scaffolding
  • Vue 3 integrations
  • Logging
  • Authentication/Authorization
  • Parsing + sending emails

Pioneers

Below are "pioneers" who make our work easier and deserve more credit than just an import in the go.mod:

Directories ΒΆ

Path Synopsis
http
ctx
The package ctx defines how keys in a *http.Request.Context should behave and a way for storing and retrieving those keys for wider use in the application.
The package ctx defines how keys in a *http.Request.Context should behave and a way for storing and retrieving those keys for wider use in the application.
example
Package main provides a toy example use of Trails' http stack.
Package main provides a toy example use of Trails' http stack.
middleware
The middleware package defines what a middleware is in trails and a set of basic middlewares.
The middleware package defines what a middleware is in trails and a set of basic middlewares.
resp
Package resp provides a high-level API for responding to HTTP requests.
Package resp provides a high-level API for responding to HTTP requests.
router
Package router defines what an HTTP server is and a default implementation of it.
Package router defines what an HTTP server is and a default implementation of it.
template/templatetest
Package templatetest exposes a mock fs.FS that implements basic file operations.
Package templatetest exposes a mock fs.FS that implements basic file operations.
Package logger defines how to log in trails as well as provide a default implementation of that interface.
Package logger defines how to log in trails as well as provide a default implementation of that interface.
Package postgres manages our database connection.
Package postgres manages our database connection.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL