Skip to content

SpineA framework that doesn't hide the request process

Spine reveals how a request is interpreted, in what order it executes, when business logic is invoked, and how the response is finalized through an explicit execution pipeline. Controllers express only use-cases, and the runtime is responsible for all execution decisions.

IoC Container · Execution Pipeline · Interceptor Chain
Execution Structure without Magic

Key Features

1. Familiar Structure

If you are a Spring or NestJS developer, start right away. Controller → Service → Repository layered architecture with constructor injection and interceptor chains. Borrowing the familiar enterprise structure, but execution is handled by Spine's explicit pipeline.

go
func NewUserService(repo *UserRepository) *UserService {
    return &UserService{repo: repo}
}

func NewUserController(svc *UserService) *UserController {
    return &UserController{svc: svc}
}

2. Fast Start

Notice

Global localization is planned for the future. If you need it, please leave your opinion on the Spine Issue. We will address it quickly.

No JVM warmup required. No Node.js runtime initialization. The compiled Go binary receives requests immediately.

zsh — 80x24
➜ spine-app go run .

3. Less Code

Express dependencies with only the type system, without @Injectable, @Controller, or @Autowired.

Instead of annotations or conventions, the signature itself reveals the structure and contract.

4. Interceptor Pipeline

You can inject logic at pre-request / post-request / completion points. Place cross-cutting concerns like authentication, transactions, and logging separated from business code into the execution flow.

It provides a user experience similar to Spring's HandlerInterceptor, but the execution order is explicitly controlled by Spine's pipeline.

go
goapp.Interceptor(
    &TxInterceptor{},
    &AuthInterceptor{},
    &LoggingInterceptor{},
)

Execution flows through a single pipeline

In Spine, requests pass through a single execution pipeline without exception.

The Router only selects the target for execution, and only the Pipeline knows the execution order and flow.

What you see is what you get

No annotations, no module definitions

main.go

go
// main.go
func main() {
    app := spine.New()
    
    // ✅ Dependencies automatically resolved just by registering constructors
    // ✅ Can be registered in any order
    app.Constructor(NewUserRepository, NewUserService, NewUserController)
    
    routes.RegisterUserRoutes(app)
    app.Run(":8080")
}

routes.go

go
// routes.go
func RegisterUserRoutes(app spine.App) {
    // ✅ Explicit connection between route and handler
    // ✅ Identify which method is for which path at a glance
    app.Route("GET", "/users", (*UserController).GetUser)
    app.Route("POST", "/users", (*UserController).CreateUser)
}

controller.go

go
// controller.go
// ✅ No annotations — Pure Go struct
// ✅ Easy to mock during testing
type UserController struct {
    svc *UserService
}

// ✅ Constructor parameter = Dependency declaration
// ✅ No hidden magic
func NewUserController(svc *UserService) *UserController {
    return &UserController{svc: svc}
}

// ✅ Function signature is the API spec
// ✅ Clear input (query.Values) and output (UserResponse, error)
func (c *UserController) GetUser(ctx context.Context, q query.Values) (UserResponse, error) {
    return c.svc.Get(ctx, q.Int("id", 0))
}

service.go

go
// service.go
// ✅ No annotations
type UserService struct {
    repo *UserRepository
}

func NewUserService(repo *UserRepository) *UserService {
    return &UserService{repo: repo}
}

repository.go

go
// repository.go
// ✅ No annotations
type UserRepository struct {
    db *bun.DB
}

func NewUserRepository(db *bun.DB) *UserRepository {
    return &UserRepository{db: db}
}

"A framework that doesn't hide the request process"