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 about Move out when the work becomes
explicit app construction through DefaultConfig() / AppConfig and AppDependencies feature-specific wiring that belongs in app-local code or an extension
one canonical lifecycle path: Prepare + Server + Shutdown debug metadata transport and runtime snapshot payloads in x/observability/devtools
route binding through App.AddRoute, App.Get, App.Post, and named-route URL lookup raw router replacement, feature catalogs, or capability-owned runtime behavior
passive dependency injection such as logger wiring logger 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.

Field Default Description
Addr :8080 Listen address
TLS.Enabled false Enable TLS
TLS.CertFile "" Path to TLS certificate
TLS.KeyFile "" Path to TLS private key
Router.MethodNotAllowed false Return 405 with Allow header on method mismatch
ReadTimeout 30s Max duration for reading entire request
ReadHeaderTimeout 5s Max duration for reading request headers
WriteTimeout 30s Max duration for writing response
IdleTimeout 60s Max idle time for keep-alive connections
MaxHeaderBytes 1 MiB Max size of request headers
HTTP2Enabled true Keep HTTP/2 support enabled
DrainInterval 500ms Log 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.