Skip to content

Options

ShiftAPI uses a layered option system. Options configure the API, groups, and individual routes.

Each option type is named for the method it can be passed to.

TypeWhere it worksExamples
OptionNew, Group, and every route methodWithError, WithMiddleware, WithResponseHeader
APIOptionNewWithInfo, WithLogger, WithBadRequestError, WithInternalServerError
GroupOptionGroup(all Option values)
RouteOptionHandle, HandleRaw, HandleSSE, HandleWSWithRouteInfo
HandleOptionHandle, HandleRawWithStatus, WithContentType
SSEOptionHandleSSESSESends
WSOptionHandleWSWithWSAcceptOptions

Option is the widest: it satisfies every other interface, so WithError and WithMiddleware work everywhere. RouteOption sits one level down, covering all four route methods but not New or Group. The narrow types are restricted at compile time, so passing SSESends to Handle fails to build.

Use ComposeOptions to bundle multiple Option values into a single reusable option that works at any level:

func WithAuth() shiftapi.Option {
return shiftapi.ComposeOptions(
shiftapi.WithMiddleware(authMiddleware),
shiftapi.WithError[*AuthError](http.StatusUnauthorized),
)
}
// Works at any level
api := shiftapi.New(WithAuth())
v1 := api.Group("/api/v1", WithAuth())
v1.Handle("GET /secret", getSecret, WithAuth())

To mix shared options with level-specific options, use the corresponding compose function:

FunctionAcceptsReturns
ComposeOptions...OptionOption
ComposeAPIOptions...APIOptionAPIOption
ComposeGroupOptions...GroupOptionGroupOption
ComposeHandleOptions...HandleOptionHandleOption

Since Option implements all level interfaces, you can pass it to any of the level-specific compose functions alongside level-specific options:

createOpts := shiftapi.ComposeHandleOptions(
shiftapi.WithStatus(http.StatusCreated), // HandleOption
shiftapi.WithError[*ConflictError](http.StatusConflict), // Option (also a HandleOption)
)
api.Handle("POST /users", createUser, createOpts)

Use Group to register routes under a common path prefix with shared options:

v1 := api.Group("/api/v1",
shiftapi.WithError[*RateLimitError](http.StatusTooManyRequests),
shiftapi.WithMiddleware(auth),
)
v1.Handle("GET /users", listUsers) // GET /api/v1/users
v1.Handle("POST /users", createUser) // POST /api/v1/users

Groups can be nested. Error types and middleware are inherited by child groups:

admin := v1.Group("/admin",
shiftapi.WithError[*ForbiddenError](http.StatusForbidden),
shiftapi.WithMiddleware(adminOnly),
)
admin.Handle("GET /stats", getStats) // GET /api/v1/admin/stats
// inherits RateLimitError (429) + auth middleware from v1
// adds ForbiddenError (403) + adminOnly middleware

Options merge across all three levels: New() (API-wide) → Group() (group-level) → route registration (route-level).

A group is also the unit of route composition. A function that registers routes should take a *shiftapi.Group, leaving the prefix and shared options to the caller:

func UserRoutes(g *shiftapi.Group) {
g.Handle("GET /users", listUsers)
g.Handle("POST /users", createUser)
}
UserRoutes(api.Group("/api/v1")) // GET /api/v1/users, POST /api/v1/users

Pass api.Group("") to mount at the root. Take *shiftapi.Group rather than *shiftapi.API in these functions. Go does not allow generic methods on interfaces, so no single interface covers both types, and a group works everywhere an API does.