Skip to content

Core Primer

Open this page after Stable Roots when the change still belongs to the default service path and the real question has narrowed to the HTTP application kernel.

core owns app construction, route attachment, middleware attachment, and server lifecycle. It is the right home when the change should remain part of the smallest canonical service shape every Plumego service is expected to understand.

app := core.New(core.DefaultConfig(),
core.AppDependencies{Logger: plog.NewLogger()})
_ = app.Get("/ping", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = contract.WriteResponse(w, r, http.StatusOK,
map[string]string{"status": "ok"}, nil)
}))
app.Run()
  • you are assembling an application from explicit routes and middleware
  • you are changing Prepare, Server, or Shutdown lifecycle behavior
  • you are working on app construction or shared runtime dependency wiring
  • the change is really about route matching rather than the app kernel
  • the change introduces feature catalogs, plugin containers, or tenant policy
  • the work is persistence behavior or debug-tool payload design

First files to read in the current repository

Section titled “First files to read in the current repository”
  1. core/module.yaml
  2. core/app.go
  3. core/dependencies.go
  4. reference/standard-service/internal/app/app.go
Keep it in core when the work is aboutMove out when the work becomes
explicit app construction through DefaultConfig() / AppConfig and AppDependenciesfeature-specific wiring that belongs in app-local code or an extension
one canonical lifecycle path: Prepare + Server + Shutdowndebug metadata transport and runtime snapshot payloads in x/observability/devtools
route binding through App.AddRoute, App.Get, App.Post, and named-route URL lookupraw router replacement, feature catalogs, or capability-owned runtime behavior
passive dependency injection such as logger wiringlogger subsystem ownership, flush policy, or readiness policy that stays app-local
import "github.com/spcent/plumego/core"
cfg := core.DefaultConfig() // baseline: :8080, 30s timeouts, HTTP/2 on
cfg.Addr = ":9090" // override any field before passing to New
app := core.New(cfg, core.AppDependencies{
Logger: myLogger, // omit to discard logs
})

DefaultConfig returns a ready-to-use AppConfig. Override only what differs.

FieldDefaultDescription
Addr:8080Listen address
TLS.EnabledfalseEnable TLS
TLS.CertFile""Path to TLS certificate
TLS.KeyFile""Path to TLS private key
Router.MethodNotAllowedfalseReturn 405 with Allow header on method mismatch
ReadTimeout30sMax duration for reading entire request
ReadHeaderTimeout5sMax duration for reading request headers
WriteTimeout30sMax duration for writing response
IdleTimeout60sMax idle time for keep-alive connections
MaxHeaderBytes1 MiBMax size of request headers
HTTP2EnabledtrueKeep HTTP/2 support enabled
DrainInterval500msLog interval for in-flight connections during drain
app.Get("/api/hello", http.HandlerFunc(handler.Hello))
app.Post("/api/items", http.HandlerFunc(handler.CreateItem))
app.Put("/api/items/:id", http.HandlerFunc(handler.UpdateItem))
app.Delete("/api/items/:id", http.HandlerFunc(handler.DeleteItem))
app.Patch("/api/items/:id", http.HandlerFunc(handler.PatchItem))
// method + path in one call
app.AddRoute(http.MethodGet, "/api/status", http.HandlerFunc(handler.Status))
// named route for reverse lookup
app.AddRoute(http.MethodGet, "/api/users/:id", http.HandlerFunc(handler.GetUser),
router.WithRouteName("user-detail"))
url := app.URL("user-detail", "id", "42") // → "/api/users/42"
import (
"github.com/spcent/plumego/middleware/requestid"
"github.com/spcent/plumego/middleware/recovery"
"github.com/spcent/plumego/middleware/accesslog"
)
recoveryMw, err := recovery.Middleware(recovery.Config{Logger: app.Logger()})
if err != nil {
return err
}
accesslogMw, err := accesslog.Middleware(accesslog.Config{Logger: app.Logger()})
if err != nil {
return err
}
app.Use(requestid.Middleware(), recoveryMw, accesslogMw)

Use must be called before Prepare. Middleware runs in registration order.

Combined path — for most services:

app.Run() // Prepare + ListenAndServe in one call; logs and exits on error

Explicit path — when you need to inspect or wrap the server before it starts (TLS, connection tracking, graceful shutdown with OS signals):

if err := app.Prepare(); err != nil {
log.Fatalf("prepare: %v", err)
}
srv, err := app.Server()
if err != nil {
log.Fatalf("get server: %w", err)
}
// Handle OS signals for graceful shutdown
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer stop()
serverErr := make(chan error, 1)
go func() { serverErr <- srv.ListenAndServe() }()
select {
case err := <-serverErr:
log.Fatalf("server stopped: %v", err)
case <-ctx.Done():
// signal received — begin graceful drain
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
_ = app.Shutdown(shutdownCtx)

Prepare freezes the route table and builds the handler chain. Server returns the *http.Server. Shutdown drains in-flight connections and respects the context deadline. See Graceful Shutdown for the complete pattern including resource cleanup.

core is often misread as the place for anything that feels foundational. The current repository says otherwise: core is a kernel, not a feature catalog. If the change makes the kernel learn capability semantics, it is probably in the wrong home.