Help us shape the future of Go! Take the 2026 Go Developer Survey

bifrost

package module
v1.4.0-alpha.17 Latest Latest
Warning

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

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 33 Imported by: 2

README

Bifrost

Bifrost is a file-based React App Router served by Go. Vite builds the client, Bun renders React on the server, and Go owns routing, loaders, and HTTP handlers.

Requirements

Go 1.25+, Bun, Linux or macOS.

Create and run

go install github.com/3-lines-studio/bifrost/cmd/bifrost@latest
bifrost init myapp
cd myapp
bifrost dev .

init writes an App Router project and an AGENTS.md, then runs go mod tidy and bun install. --no-install skips those installs.

Command Does
bifrost dev [dir] Development server with HMR on :8080 (BIFROST_ADDR overrides it).
bifrost build [dir] Builds .bifrost/bifrost-app and prints the route table.
bifrost routes [dir] Prints the route table; --json is the machine-readable form.

Run a build with ./.bifrost/bifrost-app (or BIFROST_ADDR=:3000 ./.bifrost/bifrost-app). The binary embeds the frontend.

Routes

Routes live in app/, or in the project root when there is no app/. A directory becomes a route when it contains page.tsx; its path is the URL path.

Path URL
app/page.tsx /
app/posts/page.tsx /posts
app/posts/post-id_/page.tsx /posts/{post-id}
app/docs/slug__/page.tsx /docs/{slug...}
app/marketing~/about/page.tsx /about
app/_components/Card.tsx never routed

One trailing _ is a one-segment parameter. Two are a catch-all, which must be the last segment. A trailing ~ groups files without changing the URL, and a leading _ keeps a directory out of routing. Bracketed names such as [slug] fail the build.

Files

  • page.tsx exports Page (or a default export) and optionally Head.
  • page.go next to page.tsx exports Load(r *http.Request) (any, error) and optionally Cache() string.
  • page.go without page.tsx is a Go page: it exports typed Load and Page, and optionally Layout, Error, NotFound, and Cache.
  • layout.tsx exports Layout, wraps the routes below it, and stays mounted across navigation.
  • layout.go exports Layout(content g.Node) g.Node or LayoutContext(content g.Node, r *http.Request) g.Node and wraps Go pages below it.
  • template.tsx exports Template, wraps like a layout, and remounts on navigation.
  • loading.tsx exports Loading, shown while a navigation to that subtree runs.
  • error.tsx exports Error({ error, reset }); reset re-fetches the route.
  • not-found.tsx exports NotFound.
  • error.go exports Error(err error, r *http.Request) g.Node for Go pages below it.
  • not-found.go exports NotFound(r *http.Request) g.Node for Go pages below it. The root file also handles unmatched URLs.
  • route.go exports any of Get, Post, Put, Patch, Delete, Head, Options with http.ResponseWriter and *http.Request. Missing methods answer 405.
  • middleware.go exports Middleware(next http.Handler) http.Handler and wraps the subtree.
  • server.go exports Serve(ctx context.Context, handler http.Handler) error; it is only valid at the route root.
  • public/ is served as-is.

Page and loader

export default function Page({ params, title }: { params: { slug: string }; title: string }) {
  return <article><h1>{title}</h1><p>{params.slug}</p></article>;
}
package slug

import "net/http"

func Load(r *http.Request) (any, error) {
	return map[string]any{"title": "Post " + r.PathValue("slug")}, nil
}

Pages receive params, searchParams, and pathname merged into the loader props. params keys by folder name, a catch-all parameter is an array of segments, and a repeated query key is an array. A loader must return a map or a bifrost.PageData whose Props is a map. Return bifrost.NotFound() for a 404; a loader redirect keeps its Location.

A loader may also return bifrost.PageData{Props: props, Document: bifrost.Document{Lang: "pt-BR", Class: "dark", Dir: "ltr"}} to set the root document attributes.

Go pages

A directory with only page.go renders a Go page. The file contains the loader, the page, and its components:

package dashboard

import (
	"net/http"

	g "maragu.dev/gomponents"
	h "maragu.dev/gomponents/html"
)

type Props struct {
	Title string
}

func Load(r *http.Request) (Props, error) {
	return Props{Title: r.URL.Query().Get("title")}, nil
}

func Page(props Props) g.Node {
	return h.HTML(
		h.Head(h.TitleEl(g.Text(props.Title))),
		h.Body(h.H1(g.Text(props.Title))),
	)
}

Page returns a g.Node. Components are plain Go functions in the same file and receive typed arguments. Bifrost writes the doctype.

A layout.go in the same directory or an ancestor wraps the page:

func Layout(content g.Node) g.Node {
	return h.HTML(
		h.Head(h.TitleEl(g.Text("Dashboard"))),
		h.Body(h.Main(content)),
	)
}

Inherited layouts apply from the outside in. With a layout, Page returns the content node; without one, Page returns the document. Use LayoutContext(content, r) when the layout needs params, query values, or the pathname. A page can also export Layout(content, props) g.Node in page.go; that page-specific layout takes precedence and skips inherited layout.go files.

Optional exports:

  • Error(err error, r *http.Request) g.Node and NotFound(r *http.Request) g.Node in page.go render loader errors. Otherwise Bifrost uses the nearest error.go and not-found.go, then falls back to the root not-found. bifrost.Status(...), bifrost.NotFound(), and bifrost.Redirect(...) set the status.
  • A root not-found.go also renders unmatched URLs with 404.
  • Cache() string sets Cache-Control.
  • Load receives the request, so params, query values, and the pathname come from r.PathValue, r.URL.Query, and r.URL.Path. A catch-all parameter is a string with slash-separated segments.

App Router middleware.go wraps Go pages as well. A Go-only app does not need Bun or Vite: bifrost build and bifrost dev compile and restart the Go app, and dev rebuilds reload the page. Bifrost load and render hooks observe Go page Load and render calls; response hooks and middleware wrap the page handler. The example/gomponents-app app exercises typed props, nested layouts with request context, nested error and not-found views, Alpine, vanilla CSS, dynamic params, middleware, cache, and a root not-found page. It links a hand-written public/styles.css; Vite is only needed by React client entries, not by Go pages.

Go pages are full HTML. Add Turbo Drive to the layout for client navigation; the example does. Without it, links do normal page loads. Go pages also answer Accept: application/vnd.bifrost.data+json with the loader props as JSON; that is a data endpoint, not a client router, and it exposes loader props to the caller. Go pages are server-rendered only: there is no static export and no loading.tsx equivalent; Load runs synchronously before Page.

The generator validates the arity of Load, Page, Layout, LayoutContext, Error, NotFound, and Cache before generating the handler. The Go compiler validates parameter and result types.

Go pages and React pages can coexist. A page.go next to page.tsx is still its loader. Shared Go layouts, errors, and not-found views live in layout.go, error.go, and not-found.go; page-specific layout/error functions can stay in page.go.

Route handlers

package search

import "net/http"

func Get(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	_, _ = w.Write([]byte(`{"hits":[]}`))
}

Pages and handlers share the same tree and middleware.

Metadata

export const metadata = { title: "Posts", description: "All posts" };

Layouts and pages merge metadata from the outside in, so a page overrides one key and inherits the rest. generateMetadata(props) may be async and receives the page props. Supported keys include title, description, keywords, alternates.canonical, robots.index, robots.follow, and openGraph.

Head is optional and only valid in page.tsx:

export function Head() {
  return <title>Posts</title>;
}

A page that exports Head cannot also export metadata.

Navigation

After the first render, internal <a href> links navigate in place without reloading the document for React pages. Go pages can use the same idea by including Turbo Drive in their layout; the gomponents example does. Layouts stay mounted; page state resets when the pathname changes.

import { Link, navigate, refresh, useParams, usePathname, useSearchParams, useRouter } from "virtual:bifrost/navigation";

export function Actions() {
  const router = useRouter();
  const tab = useSearchParams().get("tab");
  const pathname = usePathname();
  const params = useParams();
  return (
    <nav>
      <Link href="/posts">Posts</Link>
      <button onClick={() => navigate("/posts")}>Go</button>
      <button onClick={() => router.back()}>Back</button>
      <button onClick={async () => { await refresh(); }}>Refresh</button>
    </nav>
  );
}

navigate, replace, and refresh return Promise<void> and are called from events or effects. They cannot be called during rendering. useRouter() exposes push, replace, refresh, back, and forward.

bifrost init writes the virtual:bifrost/navigation types to bifrost.d.ts.

Styles and assets

Import a stylesheet from a layout:

import type { ReactNode } from "react";
import "./style.css";

export function Layout({ children }: { children: ReactNode }) {
  return <main>{children}</main>;
}

bifrost init sets up Tailwind in app/style.css; Vite configuration lives in vite.config.ts.

Documentation

Index

Constants

View Source
const DataMediaType = dataMediaType
View Source
const GoPageDefaultCacheControl = defaultDocumentCacheControl

Variables

View Source
var ErrRendererBusy = errors.New("bifrost: renderer is busy")
View Source
var Version = detectedVersion()

Functions

func Building added in v1.1.0

func Building() bool

func DataRequested

func DataRequested(request *http.Request) bool

func ErrorStatus added in v1.3.0

func ErrorStatus(err error) (int, bool)

func IsRedirect added in v1.3.0

func IsRedirect(err error) bool

func NotFound added in v1.1.0

func NotFound(causes ...error) error

func ObserveLoad

func ObserveLoad(ctx context.Context, started time.Time, err error)

func ObserveRender

func ObserveRender(ctx context.Context, started time.Time, err error)

func RecoverPage

func RecoverPage(w http.ResponseWriter, r *http.Request)

func Redirect added in v1.1.0

func Redirect(url string, statuses ...int) error

func Status added in v1.3.0

func Status(status int, cause error) error

func WriteData

func WriteData(w http.ResponseWriter, pattern string, props any, status int) error

func WriteDataError

func WriteDataError(w http.ResponseWriter, err error)

func WriteError

func WriteError(w http.ResponseWriter, r *http.Request, err error)

Types

type App added in v0.1.4

type App struct {
	// contains filtered or unexported fields
}

func MustNew added in v1.1.0

func MustNew(config Config) *App

func New

func New(config Config) (*App, error)

func (*App) Close added in v1.1.0

func (a *App) Close(ctx context.Context) error

func (*App) Diagnostics added in v1.1.0

func (a *App) Diagnostics() Diagnostics

func (*App) Handler added in v0.1.4

func (a *App) Handler() http.Handler

func (*App) Ready added in v1.1.0

func (a *App) Ready(ctx context.Context) error

func (*App) Register added in v1.1.0

func (a *App) Register(mux *http.ServeMux) (err error)

func (*App) ResolveMarkdown added in v1.1.1

func (a *App) ResolveMarkdown(mux *http.ServeMux) http.Handler

type AppPlugin added in v1.1.0

type AppPlugin interface {
	Name() string
	Register(*AppRegistry) error
}

type AppRegistry added in v1.1.0

type AppRegistry struct {
	// contains filtered or unexported fields
}

func (*AppRegistry) AddRoutes added in v1.1.0

func (r *AppRegistry) AddRoutes(routes ...Route) error

func (*AppRegistry) AssetHeaders added in v1.1.0

func (r *AppRegistry) AssetHeaders(hook AssetHeaderHook) error

func (*AppRegistry) HandleErrors added in v1.1.0

func (r *AppRegistry) HandleErrors(handler ErrorHandler) error

func (*AppRegistry) OnLoad added in v1.1.0

func (r *AppRegistry) OnLoad(hook LoadHook) error

func (*AppRegistry) OnQueue added in v1.1.0

func (r *AppRegistry) OnQueue(hook QueueHook) error

func (*AppRegistry) OnRender added in v1.1.0

func (r *AppRegistry) OnRender(hook RenderHook) error

func (*AppRegistry) OnResponse added in v1.1.0

func (r *AppRegistry) OnResponse(hook ResponseHook) error

func (*AppRegistry) Use added in v1.1.0

func (r *AppRegistry) Use(middleware Middleware) error

type AssetHeaderHook added in v1.1.0

type AssetHeaderHook func(http.Header, bool)

type Config added in v1.1.0

type Config struct {
	SourceRoot string
	Routes     []Route
	AppPlugins []AppPlugin

	Assets fs.FS

	RenderConcurrency int
	RenderQueue       int

	Limits Limits
	Logger *slog.Logger
}

type Diagnostics added in v1.1.0

type Diagnostics struct {
	SpecHash   string
	Routes     []RouteInfo
	AppPlugins []string
	Production bool
}

type Document added in v1.1.0

type Document struct {
	Lang  string
	Class string
	Dir   string
}

type ErrorHandler added in v1.1.0

type ErrorHandler func(http.ResponseWriter, *http.Request, error)

type Generator added in v1.1.0

type Generator func(context.Context) ([]StaticPage, error)

type Limits added in v1.1.0

type Limits struct {
	MaxPropsBytes    int
	MaxHeadBytes     int
	MaxFrameBytes    int
	MaxMarkdownBytes int
}

type LoadEvent added in v1.1.0

type LoadEvent struct {
	Pattern  string
	Duration time.Duration
	Err      error
}

type LoadHook added in v1.1.0

type LoadHook func(context.Context, LoadEvent)

type Loader added in v1.1.0

type Loader func(*http.Request) (any, error)

type Middleware added in v1.1.0

type Middleware func(http.Handler) http.Handler

type PageData added in v1.1.0

type PageData struct {
	Props          any
	Document       Document
	Status         int
	ErrorFallbacks int
}

type QueueEvent added in v1.1.0

type QueueEvent struct {
	Pattern string
	Wait    time.Duration
	Err     error
}

type QueueHook added in v1.1.0

type QueueHook func(context.Context, QueueEvent)

type RawProps added in v1.1.0

type RawProps json.RawMessage

type RenderEvent added in v1.1.0

type RenderEvent struct {
	Pattern  string
	Duration time.Duration
	Err      error
}

type RenderHook added in v1.1.0

type RenderHook func(context.Context, RenderEvent)

type ResponseEvent added in v1.1.0

type ResponseEvent struct {
	Pattern  string
	Status   int
	Bytes    int64
	Duration time.Duration
	Err      error
}

type ResponseHook added in v1.1.0

type ResponseHook func(context.Context, ResponseEvent)

type Route added in v0.1.4

type Route struct {
	// contains filtered or unexported fields
}

func Client added in v1.1.0

func Client(pattern, view string) Route

func Handler

func Handler(pattern string, handler http.Handler) Route

func Server added in v1.1.0

func Server(pattern, view string, load Loader) Route

func Static added in v1.1.0

func Static(pattern, view string, generate Generator) Route

func (Route) WithCache added in v1.3.10

func (r Route) WithCache(cacheControl string) Route

func (Route) WithNavigation added in v1.3.8

func (r Route) WithNavigation() Route

type RouteInfo added in v1.1.0

type RouteInfo struct {
	Pattern string
	View    string
	Kind    string
}

type StaticPage added in v1.1.0

type StaticPage struct {
	Path     string
	Props    any
	Document Document
}

Jump to

Keyboard shortcuts

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