spine.App
API reference for the main application interface.
Overview
App is the entry point for Spine applications. It is responsible for registering constructors, defining routes, configuring interceptors, registering event consumers, and running the server.
import "github.com/NARUBROWN/spine"Interface Definition
type App interface {
// Declare Constructors
Constructor(constructors ...any)
// Declare Route
Route(method string, path string, handler any, opts ...router.RouteOption)
// Declare Interceptors
Interceptor(interceptors ...core.Interceptor)
// HTTP Transport Extension (Echo, etc.)
Transport(fn func(any))
// Run
Run(opts boot.Options) error
// Return Event Consumer Registry
Consumers() *consumer.Registry
}Constructors
New
func New() AppCreates a new Spine application instance.
Returns
App- Application instance
Example
app := spine.New()
app.Run(boot.Options{
Address: ":8080",
HTTP: &boot.HTTPOptions{},
})Methods
Constructor
Constructor(constructors ...any)Registers constructor functions to the IoC Container. Registered constructors are used for dependency injection.
Parameters
constructors- Constructor functions (variadic)
Constructor Rules
- Must be a function
- Must return exactly one value
- Parameters must be other registered types (dependencies)
Example
// Constructor without dependencies
func NewUserRepository() *UserRepository {
return &UserRepository{}
}
// Constructor with dependencies
func NewUserController(repo *UserRepository) *UserController {
return &UserController{repo: repo}
}
// Event Consumer Constructor
func NewOrderConsumer() *OrderConsumer {
return &OrderConsumer{}
}
app.Constructor(
NewUserRepository,
NewUserController,
NewOrderConsumer,
)Route
Route(method string, path string, handler any, opts ...router.RouteOption)Registers an HTTP route.
Parameters
method- HTTP method ("GET","POST","PUT","DELETE", etc.)path- URL path pattern. Define path parameters with:paramformathandler- Controller method expressionopts- Route options (optional)
Path Patterns
/users- Static path/users/:id- Single parameter/users/:userId/posts/:postId- Multiple parameters
Example
// Basic Routes
app.Route("GET", "/users", (*UserController).List)
app.Route("GET", "/users/:id", (*UserController).GetUser)
app.Route("POST", "/users", (*UserController).CreateUser)
app.Route("PUT", "/users/:id", (*UserController).UpdateUser)
app.Route("DELETE", "/users/:id", (*UserController).DeleteUser)
// Nested Paths
app.Route("GET", "/users/:userId/posts/:postId", (*PostController).GetPost)Route Options
Use route.WithInterceptors to apply Interceptors to specific routes only.
import "github.com/NARUBROWN/spine/pkg/route"
// Apply Route-specific Interceptor
app.Route(
"GET",
"/users/:id",
(*UserController).GetUser,
route.WithInterceptors(&LoggingInterceptor{}),
)
// Apply Multiple Interceptors
app.Route(
"POST",
"/admin/users",
(*AdminController).CreateUser,
route.WithInterceptors(
&AuthInterceptor{},
&AdminRoleInterceptor{},
),
)Interceptor
Interceptor(interceptors ...core.Interceptor)Registers global Interceptors. PreHandle is executed in registration order, while PostHandle and AfterCompletion are executed in reverse order.
Parameters
interceptors- Interceptor instances (variadic)
Execution Order
- Global Interceptors (Registration order)
- Route Interceptors (Registration order)
- Controller Execution
- Route Interceptors PostHandle (Reverse order)
- Global Interceptors PostHandle (Reverse order)
Example
app.Interceptor(
cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "OPTIONS"},
AllowHeaders: []string{"Content-Type"},
}),
&LoggingInterceptor{},
&AuthInterceptor{},
)Transport
Transport(fn func(any))Registers an HTTP Transport (Echo) extension hook. Allows direct access to the Echo instance to add middlewares or configurations.
Parameters
fn- Callback function receiving the Echo instance
Example
import "github.com/labstack/echo/v4"
import "github.com/labstack/echo/v4/middleware"
app.Transport(func(e any) {
echo := e.(*echo.Echo)
// Add Echo Middleware
echo.Use(middleware.Recover())
echo.Use(middleware.RequestID())
// Serve Static Files
echo.Static("/static", "public")
})Consumers
Consumers() *consumer.RegistryReturns the event consumer registry. Registers handlers to receive events from message brokers like Kafka or RabbitMQ.
Returns
*consumer.Registry- Consumer registry
Example
// Register Event Consumer
app.Consumers().Register(
"order.created", // Topic/Event Name
(*OrderConsumer).OnCreated, // Handler Method
)
app.Consumers().Register(
"stock.created",
(*StockConsumer).OnCreated,
)Consumer Handler
Consumer handlers have a signature similar to HTTP controllers.
type OrderConsumer struct{}
func NewOrderConsumer() *OrderConsumer {
return &OrderConsumer{}
}
// Event Handler
func (c *OrderConsumer) OnCreated(
ctx context.Context,
eventName string,
event OrderCreated,
) error {
log.Println("Event received:", eventName)
log.Println("Order ID:", event.OrderID)
return nil
}
// Event DTO
type OrderCreated struct {
OrderID int64 `json:"order_id"`
At time.Time `json:"at"`
}Run
Run(opts boot.Options) errorStarts the application. runs the HTTP server and event consumer runtime together.
Parameters
opts- Boot options (boot.Options)
Returns
error- Error if server failed to start
Example
if err := app.Run(boot.Options{
Address: ":8080",
HTTP: &boot.HTTPOptions{},
}); err != nil {
log.Fatal(err)
}boot.Options
Application bootstrap options.
import "github.com/NARUBROWN/spine/pkg/boot"Struct Definition
type Options struct {
// Address to bind the server to (e.g., ":8080")
Address string
// Whether to enable Graceful Shutdown
EnableGracefulShutdown bool
// Maximum wait time during Graceful Shutdown
ShutdownTimeout time.Duration
// Kafka Event Infrastructure Configuration
// If nil, Kafka is not configured
Kafka *KafkaOptions
// RabbitMQ Event Infrastructure Configuration
// If nil, RabbitMQ is not configured
RabbitMQ *RabbitMqOptions
// HTTP Runtime Configuration
// If nil, HTTP server is not started
HTTP *HTTPOptions
}
/*
HTTP Runtime Options.
Affects only the HTTP request execution flow.
*/
type HTTPOptions struct {
// HTTP API Global Prefix (e.g., "/api/v1")
// If empty, no prefix is applied.
// Must start with '/' and cannot contain path parameters or wildcards.
GlobalPrefix string
// Disable Echo's default panic recover middleware.
// If true, panics will not be recovered and might crash the server.
DisableRecover bool
}// ✓ Valid
HTTP: &boot.HTTPOptions{GlobalPrefix: "/api/v1"}
HTTP: &boot.HTTPOptions{GlobalPrefix: "/v2"}
// ✗ Panic occurs
HTTP: &boot.HTTPOptions{GlobalPrefix: "api"} // Doesn't start with '/'
HTTP: &boot.HTTPOptions{GlobalPrefix: "/api/:ver"} // Contains path parameter
HTTP: &boot.HTTPOptions{GlobalPrefix: "/api/*"} // Contains wildcardBasic Usage
app.Run(boot.Options{
Address: ":8080",
HTTP: &boot.HTTPOptions{},
})Graceful Shutdown
app.Run(boot.Options{
Address: ":8080",
EnableGracefulShutdown: true,
ShutdownTimeout: 10 * time.Second,
HTTP: &boot.HTTPOptions{
GlobalPrefix: "/api/v1/",
},
})When Graceful Shutdown is enabled:
- Receives
SIGINT,SIGTERMsignals - Waits until ongoing requests are completed
- Sends Close message to WebSocket connections
- Calls
Stop()on Custom Transports - Forcefully terminates after
ShutdownTimeout(Default: 10s)
Recover Middleware
By default, Echo's panic recover middleware is enabled. It converts panics into 500 responses.
// Disable Recover
HTTP: &boot.HTTPOptions{
DisableRecover: true,
}Kafka Configuration
KafkaOptions
type KafkaOptions struct {
// List of Kafka broker addresses
Brokers []string
// Event Consumption (Consumer) Configuration
// If nil, Kafka Consumer is not enabled
Read *KafkaReadOptions
// Event Publishing (Producer) Configuration
// If nil, events are not published to Kafka
Write *KafkaWriteOptions
}
type KafkaReadOptions struct {
// Kafka Consumer Group ID
GroupID string
}
type KafkaWriteOptions struct {
// Topic Prefix to prepend to event names
TopicPrefix string
}Example
app.Run(boot.Options{
Address: ":8080",
Kafka: &boot.KafkaOptions{
Brokers: []string{"localhost:9092"},
Read: &boot.KafkaReadOptions{
GroupID: "my-consumer-group",
},
Write: &boot.KafkaWriteOptions{
TopicPrefix: "myapp.",
},
},
HTTP: &boot.HTTPOptions{},
})RabbitMQ Configuration
RabbitMqOptions
type RabbitMqOptions struct {
// RabbitMQ AMQP Connection String
// e.g., amqp://guest:guest@localhost:5672/
URL string
// Event Consumption (Consumer) Configuration
// If nil, RabbitMQ Consumer is not enabled
Read *RabbitMqReadOptions
// Event Publishing (Publisher) Configuration
// If nil, events are not published to RabbitMQ
Write *RabbitMqWriteOptions
}
type RabbitMqReadOptions struct {
// Exchange name to bind the queue to
Exchange string
}
type RabbitMqWriteOptions struct {
// Exchange name to publish events to
Exchange string
}Example
app.Run(boot.Options{
Address: ":8080",
RabbitMQ: &boot.RabbitMqOptions{
URL: "amqp://guest:guest@localhost:5672/",
Read: &boot.RabbitMqReadOptions{
Exchange: "stock-exchange",
},
Write: &boot.RabbitMqWriteOptions{
Exchange: "stock-exchange",
},
},
HTTP: &boot.HTTPOptions{},
})Event Publishing
Domain events can be published from the Controller.
DomainEvent Interface
import "github.com/NARUBROWN/spine/pkg/event/publish"
type DomainEvent interface {
Name() string
OccurredAt() time.Time
}Event Definition
type OrderCreated struct {
OrderID int64 `json:"order_id"`
At time.Time `json:"at"`
}
func (e OrderCreated) Name() string {
return "order.created"
}
func (e OrderCreated) OccurredAt() time.Time {
return e.At
}Publishing from Controller
import "github.com/NARUBROWN/spine/pkg/event/publish"
func (c *OrderController) Create(ctx context.Context, req *CreateOrderRequest) Order {
order := c.repo.Save(req)
// Publish Event
publish.Event(ctx, OrderCreated{
OrderID: order.ID,
At: time.Now(),
})
return order
}Events are published in a batch from PostExecutionHook after Controller execution completes. publish.Event() collects events into the EventBus injected into context.Context, and if the execution completes without error, they are sent via the registered Publisher (Kafka/RabbitMQ).
Full Example
package main
import (
"context"
"log"
"time"
"github.com/NARUBROWN/spine"
"github.com/NARUBROWN/spine/interceptor/cors"
"github.com/NARUBROWN/spine/pkg/boot"
"github.com/NARUBROWN/spine/pkg/event/publish"
"github.com/NARUBROWN/spine/pkg/path"
"github.com/NARUBROWN/spine/pkg/route"
"github.com/NARUBROWN/spine/pkg/ws"
)
func main() {
app := spine.New()
// Register Constructors
app.Constructor(
NewUserController,
NewOrderConsumer,
NewChatController,
)
// Register HTTP Routes
app.Route("GET", "/users", (*UserController).GetUserQuery)
app.Route("GET", "/users/:id", (*UserController).GetUser,
route.WithInterceptors(&LoggingInterceptor{}),
)
app.Route("POST", "/orders/:orderId", (*UserController).CreateOrder)
// Register Global Interceptors
app.Interceptor(
cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "OPTIONS"},
AllowHeaders: []string{"Content-Type"},
}),
)
// Register Event Consumers
app.Consumers().Register("order.created", (*OrderConsumer).OnCreated)
// Register WebSockets
app.WebSocket().Register("/ws/chat", (*ChatController).OnMessage)
// Run Server
app.Run(boot.Options{
Address: ":8080",
EnableGracefulShutdown: true,
ShutdownTimeout: 10 * time.Second,
RabbitMQ: &boot.RabbitMqOptions{
URL: "amqp://guest:guest@localhost:5672/",
Read: &boot.RabbitMqReadOptions{
Exchange: "stock-exchange",
},
Write: &boot.RabbitMqWriteOptions{
Exchange: "stock-exchange",
},
},
HTTP: &boot.HTTPOptions{},
})
}
// Controller
type UserController struct{}
func NewUserController() *UserController {
return &UserController{}
}
func (c *UserController) CreateOrder(ctx context.Context, orderId path.Int) string {
publish.Event(ctx, OrderCreated{
OrderID: orderId.Value,
At: time.Now(),
})
return "OK"
}
// Event
type OrderCreated struct {
OrderID int64 `json:"order_id"`
At time.Time `json:"at"`
}
func (e OrderCreated) Name() string { return "order.created" }
func (e OrderCreated) OccurredAt() time.Time { return e.At }
// Consumer
type OrderConsumer struct{}
func NewOrderConsumer() *OrderConsumer {
return &OrderConsumer{}
}
func (c *OrderConsumer) OnCreated(
ctx context.Context,
eventName string,
event OrderCreated,
) error {
log.Println("Event received:", eventName)
log.Println("Order ID:", event.OrderID)
return nil
}
// WebSocket
type ChatController struct{}
func NewChatController() *ChatController {
return &ChatController{}
}
func (c *ChatController) OnMessage(
connID ws.ConnectionID,
msg ws.TextPayload,
sender ws.Sender,
) {
sender.Send(ws.TextMessage, []byte("echo: "+msg.Value))
}Bootstrap Order
Initialization proceeds in the following order when Run() is called:
- Create IoC Container
- Register Constructors
- Configure Event Infrastructure (if configured)
- Kafka Publisher / Consumer
- RabbitMQ Publisher / Consumer
- Initialize Custom Transport (
Init()execution) - Start Custom Transport (
Start()in separate goroutine) - Configure HTTP Runtime (if configured)
- Configure Router and create HandlerMeta
- Resolve Route Interceptors (nil pointer → Container)
- Assert No Ambiguous Routes
- Controller Warm-up (Pre-resolve dependencies)
- Configure HTTP Pipeline (Register ArgumentResolvers, ReturnValueHandlers)
- Register Global Interceptors (Remove duplicates, resolve nil pointers)
- Configure WebSocket Runtime (if registered)
- Create WS-specific Pipeline
- Auto mount via Echo Transport Hook
- Mount Echo Adapter (Includes Recover middleware)
- Start Consumer Runtime (if configured)
- Start HTTP Server
On Graceful Shutdown
- Receive
SIGINT/SIGTERMsignals - Send Close message to WebSocket connections
- Stop Event Consumer Runtime
- Call
Stop()on Custom Transports - Shutdown HTTP Server (Wait for timeout)
- Clean up Event Publisher resources
- Exit
See Also
- Interceptor - Interceptor Interface
- Execution Pipeline - Request Processing Flow
- IoC Container - Dependency Injection
