The app package is a lightweight, modular, foundational micro-framework based on NATS for building and running the Upspeak application. Its primary aims are:
- Composable; Modular: Enable declarative, event-driven modules. Compose an application out of these modules, without the modules having to worry about macro-architectural decisions.
- Flexible deployment: Produce a single binary. Deploy as standalone, or run specific modules to create distributed deployments. Offer lightweight deployments via embedded NATS while supporting external NATS setups.
- Simplify till it hurts, without compromising functionality: Thanks, NATS, for making this possible.
- This package is tailored for Upspeak. It is not intended to be a general-purpose framework. Avoid dependending on this package for other projects. Copy/Fork if you like it that much.
- Relies on configurations from YAML/JSON files, without advanced runtime reconfiguration, at least for now.
- Not intended to support multiple message queues to keep the intra-system and inter-module communication simple; embracing NATS. Offload as much of the requirements to NATS as possible.
- App: Composes modules, and manages HTTP servers with namespaced module routes, and NATS connections. Responsible for the entire application lifecycle.
- Module: Interface for modular components with HTTP and NATS handlers.
- Publisher: Handles message publication.
- Config: Encapsulates app configuration, including NATS and HTTP settings.
- Embedded NATS Server: Runs an in-process instance.
- Health/Readiness Endpoints:
/healthz: Returns 200 if operational./readiness: Returns 200 if ready, otherwise 503.
- Lifecycle Management:
Start: Initializes modules, NATS, and HTTP server.Stop: Gracefully shuts down components.
- Create a Config File
Example config.yaml:
name: "upspeak"
nats:
embedded: true
private: false
logging: true
http:
port: 8080
modules:
example:
enabled: true
config:
key: "value"- Load Configuration
config, err := app.LoadConfig("config.yaml")
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}- Initialize the App
myApp := app.New(*config)- Add Modules
Modules implement the Module interface. Example:
type ExampleModule struct{}
func (m *ExampleModule) Name() string {
return "example"
}
func (m *ExampleModule) Init(config map[string]any) error {
return nil
}
func (m *ExampleModule) HTTPHandlers(pub app.Publisher) []app.HTTPHandler {
return []app.HTTPHandler{
{
Method: "GET",
Path: "/hello",
Handler: func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
},
},
}
}
func (m *ExampleModule) MsgHandlers(pub app.Publisher) []app.MsgHandler {
return []app.MsgHandler{
{
Subject: "example.subject",
Handler: func(msg *nats.Msg) {
fmt.Printf("Received message: %s", string(msg.Data))
},
},
}
}
// Add module at default path (/<module-name>/)
if err := myApp.AddModule(&ExampleModule{}); err != nil {
log.Fatalf("Failed to add module: %v", err)
}Based on the code above, the example module's endpoints will now be mounted at GET http://localhost:8080/example/hello.
Alternative: Custom Mount Path
You can mount modules at custom paths using AddModuleOnPath:
// Mount UI at root
if err := myApp.AddModuleOnPath(&UIModule{}, ""); err != nil {
log.Fatalf("Failed to add UI module: %v", err)
}
// Mount API at /api
if err := myApp.AddModuleOnPath(&APIModule{}, "/api"); err != nil {
log.Fatalf("Failed to add API module: %v", err)
}
// Mount v1 API (can omit leading slash)
if err := myApp.AddModuleOnPath(&V1Module{}, "v1"); err != nil {
log.Fatalf("Failed to add v1 module: %v", err)
}Path Mounting Rules:
- Empty string
""or"/"mounts at root - Leading slash is optional and normalized automatically
- Trailing slashes are removed
- Only one module can be mounted at root
- Paths cannot conflict with reserved endpoints (
/healthz,/readiness) - Root module handlers are registered last for proper catch-all routing
- Start the App
if err := myApp.Start(); err != nil {
log.Fatalf("Failed to start app: %v", err)
}- Stop the App
if err := myApp.Stop(); err != nil {
log.Printf("Failed to stop app: %v", err)
}GET /healthz: Basic liveness check.GET /readiness: Readiness check; returns 503 if not ready.
- Prefix:
UPSPEAK_ - Example:
UPSPEAK_HTTP_PORT=9090sets the HTTP port to 9090.
The framework allows flexible module mounting using AddModuleOnPath. This is particularly useful for serving UI applications at root or creating versioned APIs.
func main() {
config, _ := app.LoadConfig("config.yaml")
myApp := app.New(*config)
// Mount UI module at root (/) for clean URLs
if err := myApp.AddModuleOnPath(&ui.Module{}, ""); err != nil {
log.Fatal(err)
}
// Mount API modules at specific paths
if err := myApp.AddModuleOnPath(&api.Module{}, "/api"); err != nil {
log.Fatal(err)
}
if err := myApp.AddModule(&writer.Module{}); err != nil {
log.Fatal(err)
}
myApp.Start()
}Resulting URL structure:
GET /→ UI Module (SPA)GET /about→ UI ModuleGET /api/users→ API ModuleGET /writer/posts→ Writer ModuleGET /healthz→ Health checkGET /readiness→ Readiness check
The framework registers modules in two passes:
- First pass: All non-root modules are registered
- Second pass: Root module (if any) is registered last
This ensures that specific routes (like /api/users) take precedence over catch-all routes (like /* for SPA routing).
- Add support for SQL DBs, object stores, and KV stores.
- Enable dynamic reconfiguration.
- Improve observability with metrics endpoints.
- Enhance lifecycle management for module reliability.
This package is part of the Upspeak project and follows its licensing terms.