datapages

command module
v0.2.3 Latest Latest
Warning

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

Go to latest
Published: Feb 28, 2026 License: MIT Imports: 6 Imported by: 0

README

Datapages

CI golangci-lint Coverage Status Go Report Card Go Reference Alpha

🧪 Alpha Software: Datapages is still in early development 🚧.
APIs are subject to change and you may encounter bugs.

A Templ + Go + Datastar web framework for building dynamic, server-rendered web applications in pure Go.

Focus on your business logic, generate the boilerplate Datapages parses your app source package and generates all the wiring. Routing, sessions and authentication, SSE streams, CSRF protection, type-safe URL and action helpers - so your application code stays clean and takes full advantage of Go's strong static typing and high performance.

Getting Started

Install
go install github.com/romshark/datapages@latest
Initialize New Project
datapages init

CLI Commands

Command Description
datapages init Initialize a new project with scaffolding and configuration.
datapages gen Parse the app model and generate the datapages package.
datapages watch Start the live-reloading development server.
datapages lint Validate the app model without generating code.
datapages version Print CLI version information.

Configuration

Datapages reads configuration from datapages.yaml in the module root:

app: app            # Path to the app source package (default)
gen:
  package: datapagesgen # Path to the generated package (default)
  prometheus: true      # Enable Prometheus metrics generation (default)
cmd: cmd/server     # Path to the server cmd package (default)

When prometheus is set to false, the generated server code will not include Prometheus imports, metric variables, or the WithPrometheus server option. Use datapages init --prometheus=false to scaffold a project without Prometheus.

The optional watch section configures the development server (host, proxy timeout, TLS, compiler flags, custom watchers, etc.).

Demo: Classifieds

This repository features a demo application resembling an online classifieds marketplace under example/classifieds. The code you'd write is in example/classifieds/app (the "source package"). The code that the generator produces is in example/classifieds/datapagesgen.

To run the demo in development mode, use:

cd example/classifieds
make dev

You can then access:

You can install k6 and run make load in the background to generate random traffic. Increase the number of virtual users (VU) to apply more load to the server when needed.

To run the demo in production mode, use:

make stage

Source Package

Generator requires a path to an application source package that must contain an App type and the type PageIndex struct.

App

The App type may optionally provide a method for custom global HTML <head> tags:

func (*App) Head(
	r *http.Request,
	session Session,
) (templ.Component, error) {
	return globalHeadTags(session.UserID), error
}

The Recover500 method allows you to recover 500 Internal Server errors to improve UX by giving better feedback. If Recover500 returns an error the server falls back to the ugly standard procedure.

func (*App) Recover500(
	err error,
	sse *datastar.ServerSentEventGenerator,
) error {
	return sse.PatchElementTempl(errorToast(err))
}
Pages

Individual pages are defined with type PageXXX struct { App *App } and special methods:

  • GET: handles GET requests.
  • POSTXXX: handles POST action requests.
  • PUTXXX: handles PUT action requests.
  • DELETEXXX: handles DELETE action requests.
  • OnXXX: subscribes to events in the SSE listener.

XXX is just a name placeholder.

Page types must only contain the exported App *App field, no more, no less. Methods can be enriched with capabilities through parameters.

URLs must be specified by a strictly formatted comment in net/http Mux pattern syntax:

The page type PageIndex (for URL /) is required.

Page types PageError500 and PageError404 are optional special error pages for the response codes 500 and 404 respectively. Otherwise datapages will use its own defaults.

Handler method parameters and their order are defined and enforced by datapages. Using unsupported parameter names and types will result in generator errors.

The GET method parameter lists must always start with r *http.Request, followed by other parameters:

func (PageIndex) GET(
	r *http.Request,
	sessionToken string, // Optional
	session Session, // Optional
	path struct{...}, // Required only when path variables are used in the URL
	query struct{...}, // Optional
	signals struct {...}, // Optional
	dispatch(
		EventSomethingHappened,
		EventSomethingElseHappened,
		//...
	) error // Optional
) (
	body templ.Component,
	head templ.Component, // Optional
	redirect string, // Optional
	redirectStatus int, // Optional
	newSession Session, // Optional
	closeSession bool, // Optional
	enableBackgroundStreaming bool, // Optional
	disableRefreshAfterHidden bool, // Optional
	err error
) {
	// ...
}

The SSE action handlers POSTXXX, DELETEXXX and PUTXXX method parameter lists must always start with r *http.Request, followed by other parameters:

// POSTActionName is <path>
func (PageIndex) POSTActionName(
	r *http.Request,
	sse *datastar.ServerSentEventGenerator, // Optional
	session Session, // Optional
	path struct{...}, // Required only when path variables are used in the URL
	query struct{...}, // Optional
	signals struct {...}, // Optional
	dispatch(
		EventSomethingHappened,
		EventSomethingElseHappened,
		//...
	) error // Optional
) error {
	// ...
}

Action handler may omit the sse parameter and instead redirect, return HTML, set/remove sessions.

// POSTActionName is <path>
func (PageIndex) POSTActionName(
	r *http.Request,
	session Session, // Optional
	path struct{...}, // Required only when path variables are used in the URL
	query struct{...}, // Optional
	signals struct {...}, // Optional
	dispatch(
		EventSomethingHappened,
		EventSomethingElseHappened,
		//...
	) error // Optional
) (
	body templ.Component, // Optional
	head templ.Component, // Optional
	redirect string, // Optional
	redirectStatus int, // Optional
	newSession Session, // Optional
	closeSession bool, // Optional
	err error,
) {
	// ...
}

All OnXXX method parameter lists must always start with the event parameter of an event type, followed by sse *datastar.ServerSentEventGenerator and other parameters. The XXX placeholder must always match the event name after the type's Event prefix.

func (PageIndex) OnSomethingHappened(
	event EventSomethingHappened,
	sse *datastar.ServerSentEventGenerator,
	session Session, // Optional
) error {
	// ...
}
Abstract Page Types

Abstract page types can be embedded in page types to share functionality across pages:

type Base struct{ App *App }

func (Base) OnSomethingHappened(
	event EventSomethingHappened,
	sse *datastar.ServerSentEventGenerator,
	session Session,
) error {
	// ...
}

// PageFoo is /foo
type PageFoo struct {
	App *App
	Base
}

func (PageFoo) GET(r *http.Request) (body templ.Component, err error) {
	return pageFoo(), nil
}

// PageBar is /bar
type PageBar struct {
	App *App
	Base
}

func (PageBar) GET(r *http.Request) (body templ.Component, err error) {
	return pageBar(), nil
}

The embeddable abstract page type must always have App *App same as concrete page types.


Example
// EventSomethingHappened is "something.happened"
type EventSomethingHappened struct {
	WhoCausedIt string `json:"who-caused-it"`
}

// PageExample is /example
type PageExample struct { App *App }

func (p PageExample) GET(r *http.Request) (body templ.Component, err error) {
	data, err := p.App.fetchData("")
	if err != nil {
		return nil, err
	}
	return examplePageTemplate(data), nil
}

// POSTInputChanged is /example/input-changed
func (p PageExample) POSTInputChanged(
	r *http.Request,
	session Session,
	signals struct {
		InputValue string `json:"inputvalue"`
	}
) (body templ.Component, err error) {
	// Patch the page with a fat morph directly on action.
	data, err := p.App.fetchData(signals.InputValue)
	if err != nil {
		return nil, err
	}
	return examplePageTemplate(data), nil
}

// POSTButtonClicked is /example/button-clicked
func (p PageExample) POSTButtonClicked(
	r *http.Request,
	session Session,
	dispatch(EventSomethingHappened) error,
) error {
	// Update everyone that something happened.
	return dispatch(EventSomethingHappened{WhoCausedIt: session.UserID})
}

func (p PageExample) OnSomethingHappened(
	event EventSomethingHappened,
	sse *datastar.ServerSentEventGenerator,
	session Session,
) error {
	// When something happens, patch the page.
	return sse.PatchElementTempl(updateTemplate())
}
Parameter: signals struct {...}
signals struct {
	Foo string `json:"foo"`
	Bar int	`json:"bar"`
}

Provides the captured Datastar signals from the page. Any named or anonymous struct is accepted, but every field must have a json struct field tag.

Parameter: path struct {...}
path struct {
	ID string `path:"id"`
}

Provides URL path parameters. These parameters must be defined in the URL comment.

Parameter: query struct {...}
query struct {
	Filter string `query:"f"`
	Limit  int	`query:"l"`
}

Provides URL query parameters. These parameters must be defined in the URL comment.

The reflectsignal struct field tag can be used to define what signal shall reflect into the query parameter:

signals struct {
	SelectedItem string `json:"selecteditem"`
},
query struct {
	SelectedItem string `query:"s" reflectsignal:"selecteditem"`
}

The above example will automatically synchronize the query parameter s with the signal selecteditem.

Parameter: session Session
session Session

Provides authentication information from cookies.

If used, must be defined at the source package level as:

type Session struct {
	UserID   string
	IssuedAt time.Time

	// Custom metadata.
	FooBar Bazz `json:"foo-bar"`
}

The Session type must have UserID string and IssuedAt time.Time fields. IssuedAt is required because CSRF protection is bound to the session issuance time. Any other field is treated as a custom payload.

Parameter: sessionToken string
sessionToken string

Provides the session token from cookies. Empty string if the request doesn't contain an authentication cookie.

If used type Session struct must be defined at the source package level.

type Session struct {
	UserID     string    `json:"sub"` // Required.
	IssuedAt   time.Time `json:"iat"` // Required.
	Expiration time.Time `json:"exp"` // Optional.
}
Parameter: sse *datastar.ServerSentEventGenerator
sse *datastar.ServerSentEventGenerator

This parameter is allowed only on POSTXXX page methods handling POST action requests and OnXXX event handler page methods. This gives you a handle to patch page elements, execute scripts, etc.

Parameter: dispatch func(...) error
dispatch func(EventXXX, /*...*/) error

This parameter provides a function for dispatching events and only accepts EventXXX types as parameters. These events can be handled by OnXXX page methods.

An event type must use json struct field tags, and be strictly commented with // EventXXX is "xxx" (where "xxx" is the NATS subject prefix):

// EventExample is "example"
type EventExample struct {
	Information string `json:"info"`
}

Events that are targeted as specific user groups only, must declare the TargetUserIDs field:

type EventMessageSent struct {
	TargetUserIDs []string `json:"-"`

	Message string `json:"message"`
	Sender  string `json:"sender"`
}

You may provide multiple event types which are dispatched in the order of definition:

dispatch func(EventTypeA, EventTypeB, EventTypeC) error

Example
// EventMessageSent is "chat.sent"
type EventMessageSent struct {
	TargetUserIDs []string `json:"-"`

	Message string `json:"message"`
	Sender  string `json:"sender"`
}

// PageChat is /chat
type PageChat struct { App *App }

func (PageChat) POSTSendMessage(
	r *http.Request,
	e EventMessageSent,
	session Session,
	signals struct {
		InputText string `json:"inputtext"`
		ChatRoom  string `json:"chatroom"`
	},
	dispatch(EventMessageSent) error,
) error {
	if !isUserAllowedToSendMessages(session.UserID) {
		return errors.New("unauthorized")
	}
	if signals.InputText == "" {
		return nil // No-op.
	}
	return dispatch(EventMessageSent{
		TargetUserIDs: chatroom.ParticipantIDs,
		Message:	   signals.InputText,
		Sender:		session.UserID,
	})
}

func (PageChat) OnMessageSent(
	event EventMessageSent,
	sse *datastar.ServerSentEventGenerator,
	session Session,
) error {
	// Use sse to patch the new message into view.
}
Parameter: metrics struct {...} (Experimental)

This feature is in its design phase and not implemented yet.

metrics struct {
	// Help description goes in this comment
	ExampleRequestsTotal interface {
		CounterAdd(delta float64, result string)
	} `name:"example_requests_total"`

	ExampleConnectionsOpen interface {
		GaugeSet(value float64)
	} `name:"example_connections_open" subsystem:"network"`

	ExampleOrderSize interface {
		HistogramObserve(value float64, )
	} `name:"order_size", buckets:"0,1,5,50,100,1000"``

	//...
},

Datapages can inject typed metric handles into page/action/event handlers, similar to signals, dispatch, etc. You declare what you need at the handler boundary, and the generator automatically defines the Prometheus collectors and registers them.

The methods of the interface define the metric kind:

Counter
interface {
	CounterAdd(label1, label2 string, /* ... */)
}
Gauge
interface {
	GaugeSet(value float64, label1, label2 string, /* ... */)
}
Histogram
interface {
	HistogramObserve(value float64, label1, label2 string, /* ... */)
}

Buckets can be defined using the bucket struct tag as a comma-separated list of values.

Return Value: body templ.Component

Specifies the Templ template to use for the contents of the page.

Return Value: head templ.Component

Specifies the Templ template to use for <head> tag of the page.

Return Value: redirect string

Allows for redirecting to different URLs.

Return Value: redirectStatus int

Specifies the redirect status code. Can only be used in combination with redirect.

Return Value: newSession Session
newSession Session

Adds response headers to set a session cookie if newSession.UserID is not empty, otherwise no-op.

Return Value: closeSession bool
closeSession bool

Closes the session and removes any session cookie if true, otherwise no-op.

Return Value error or err error

Regular error values that will be logged and followed by the error handling procedure.

GET Return Value: enableBackgroundStreaming bool

Can only be used for GET methods.

enableBackgroundStreaming bool

By default, OnXXX event handlers can't deliver updates to background tabs. If true, the SSE stream is always kept open. This prevents missed updates when the tab is inactive, but increases battery and resource usage, especially on mobile devices.

This is equivalent to datastar's openWhenHidden).

enableBackgroundStreaming=true will automatically disable the auto-refresh after hidden. If you want to prevent this, you have to explicitly add disableRefreshAfterHidden to the return values and set it to false.

GET Return Value: disableRefreshAfterHidden bool

Can only be used for GET methods.

disableRefreshAfterHidden bool

By default, Datapages refreshes the page when it becomes active again after being in the background (for example, when switching back from another tab). This is useful when enableBackgroundStreaming is false, since SSE events may be missed while the tab is inactive and the page state can become stale. You can disable this behavior by returning disableRefreshAfterHidden=true.

Datapages relies on the visibilitychange event to perform the automatic refresh.

Technical Limitations

  • For now, with CSRF protection enabled, you will not be able to use plain HTML forms, since the CSRF token is auto-injected for Datastar fetch requests (where Datastar-Request header is true). You must use Datastar actions for any sort of server interactivity.

Modules

Datapages ships pluggable modules with swappable implementations:

Development

Prerequisites
  • Go (see version in go.mod)
Contributing

See CLAUDE.md for code style, testing conventions, commit message format, and project structure.

Use the example/classifieds/ application as a real-world test fixture when developing Datapages.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
example
classifieds module
skeleton
Package skeleton provides templates for initializing a new Datapages project.
Package skeleton provides templates for initializing a new Datapages project.
internal
cmd
routepattern
Package routepattern parses net/http ServeMux route patterns.
Package routepattern parses net/http ServeMux route patterns.
modules
csrf
Package csrf defines the interface for CSRF token generation and validation.
Package csrf defines the interface for CSRF token generation and validation.
csrf/hmac
Package hmac provides an HMAC-SHA256 based CSRF token manager with BREACH-resistant random masking and sync.Pool-based allocation reuse.
Package hmac provides an HMAC-SHA256 based CSRF token manager with BREACH-resistant random masking and sync.Pool-based allocation reuse.
msgbroker/inmem
Package inmem provides an in-memory message broker with fan-out delivery semantics.
Package inmem provides an in-memory message broker with fan-out delivery semantics.
msgbroker/natsjs
Package natsjs provides a NATS JetStream backed message broker with fan-out delivery semantics.
Package natsjs provides a NATS JetStream backed message broker with fan-out delivery semantics.
sessmanager/inmem
Package inmem provides an in-memory session manager.
Package inmem provides an in-memory session manager.
sessmanager/natskv
Package natskv provides a built-in implementation of the SessionManager based on the NATS Key-Value Store.
Package natskv provides a built-in implementation of the SessionManager based on the NATS Key-Value Store.
sesstokgen
Package sesstokgen provides a default SessionTokenGenerator implementation.
Package sesstokgen provides a default SessionTokenGenerator implementation.
errsuggest
Package errsuggest provides fix suggestions for parser errors.
Package errsuggest provides fix suggestions for parser errors.
internal/methodkind
Package methodkind classifies handler method names into HTTP method kinds (GET, POST, PUT, DELETE) or event handlers.
Package methodkind classifies handler method names into HTTP method kinds (GET, POST, PUT, DELETE) or event handlers.
internal/paramvalidation
Package paramvalidation validates handler parameter structs (path, query, signals) and route-to-path consistency.
Package paramvalidation validates handler parameter structs (path, query, signals) and route-to-path consistency.
internal/structinspect
Package structinspect provides AST helpers for inspecting Go struct types and method receivers.
Package structinspect provides AST helpers for inspecting Go struct types and method receivers.
internal/structtag
Package structtag provides struct tag value extraction and cross-validation for Datapages handler parameters.
Package structtag provides struct tag value extraction and cross-validation for Datapages handler parameters.
internal/typecheck
Package typecheck provides type-checking predicates for common Go types used in Datapages handler signatures.
Package typecheck provides type-checking predicates for common Go types used in Datapages handler signatures.

Jump to

Keyboard shortcuts

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