Documentation
¶
Overview ¶
Package shiftapi provides end-to-end type safety from Go to TypeScript.
Define your API with typed Go handler functions and shiftapi automatically generates an OpenAPI 3.1 spec, validates requests, and produces a fully-typed TypeScript client — all from a single source of truth.
Quick start ¶
api := shiftapi.New()
shiftapi.Post(api, "/greet", greet)
shiftapi.ListenAndServe(":8080", api)
where greet is a typed handler:
type GreetRequest struct {
Name string `json:"name" validate:"required"`
}
type GreetResponse struct {
Hello string `json:"hello"`
}
func greet(r *http.Request, in *GreetRequest) (*GreetResponse, error) {
return &GreetResponse{Hello: in.Name}, nil
}
Struct tag conventions ¶
ShiftAPI discriminates input struct fields by their struct tags:
- json:"name" — parsed from the JSON request body (default for POST/PUT/PATCH)
- query:"name" — parsed from URL query parameters
- form:"name" — parsed from multipart/form-data (for file uploads)
- validate:"rules" — validated using github.com/go-playground/validator/v10 rules and reflected into the OpenAPI schema
- accept:"mime/type" — constrains accepted MIME types on form file fields
A single input struct can mix query and body fields:
type SearchRequest struct {
Q string `query:"q" validate:"required"`
Page int `query:"page"`
Body Filter `json:"filter"`
}
File uploads ¶
Use *multipart.FileHeader fields with the form tag for file uploads:
type UploadInput struct {
File *multipart.FileHeader `form:"file" validate:"required"`
Docs []*multipart.FileHeader `form:"docs"`
}
Error handling ¶
Return an *APIError from a handler to control the HTTP status code:
return nil, shiftapi.Error(http.StatusNotFound, "user not found")
Validation failures automatically return 422 with structured ValidationError responses.
Built-in endpoints ¶
Every API automatically serves:
- GET /openapi.json — the generated OpenAPI 3.1 spec
- GET /docs — interactive API documentation (Scalar UI)
http.Handler compatibility ¶
API implements http.Handler, so it works with any standard middleware, router, or test framework:
mux := http.NewServeMux()
mux.Handle("/api/", http.StripPrefix("/api", api))
Example ¶
package main
import (
"log"
"net/http"
"github.com/fcjr/shiftapi"
)
func main() {
api := shiftapi.New(shiftapi.WithInfo(shiftapi.Info{
Title: "My API",
Version: "1.0.0",
}))
type HelloRequest struct {
Name string `json:"name" validate:"required"`
}
type HelloResponse struct {
Message string `json:"message"`
}
shiftapi.Post(api, "/hello", func(r *http.Request, in HelloRequest) (*HelloResponse, error) {
return &HelloResponse{Message: "Hello, " + in.Name + "!"}, nil
})
log.Fatal(shiftapi.ListenAndServe(":8080", api))
}
Output:
Index ¶
- func Connect[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
- func Delete[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
- func Get[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
- func Head[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
- func ListenAndServe(addr string, api *API) error
- func Options[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
- func Patch[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
- func Post[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
- func Put[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
- func Trace[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
- type API
- type APIError
- type Contact
- type ExternalDocs
- type FieldError
- type HandlerFunc
- type Info
- type License
- type Option
- type RouteInfo
- type RouteOption
- type ValidationError
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Connect ¶
func Connect[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
Connect registers a CONNECT handler.
func Delete ¶
func Delete[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
Delete registers a DELETE handler.
func Get ¶
func Get[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
Get registers a handler for GET requests at the given path. The path follows net/http.ServeMux patterns, including wildcards like /users/{id}. Path parameters are accessible via http.Request.PathValue.
Example ¶
package main
import (
"net/http"
"github.com/fcjr/shiftapi"
)
func main() {
api := shiftapi.New()
type UserQuery struct {
ID int `query:"id" validate:"required"`
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
shiftapi.Get(api, "/user", func(r *http.Request, in UserQuery) (*User, error) {
return &User{ID: in.ID, Name: "Alice"}, nil
})
}
Output:
Example (PathParameter) ¶
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/fcjr/shiftapi"
)
func main() {
api := shiftapi.New()
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
shiftapi.Get(api, "/users/{id}", func(r *http.Request, _ struct{}) (*User, error) {
id := r.PathValue("id")
return &User{ID: id, Name: "Alice"}, nil
})
// Make a request to verify.
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/users/42", nil)
api.ServeHTTP(w, r)
fmt.Println(w.Body.String())
}
Output: {"id":"42","name":"Alice"}
func Head ¶
func Head[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
Head registers a HEAD handler.
func ListenAndServe ¶
ListenAndServe starts the HTTP server on the given address.
In production builds this is a direct call to http.ListenAndServe with zero additional overhead.
When built with -tags shiftapidev (used automatically by the Vite plugin), the following environment variables are supported:
- SHIFTAPI_EXPORT_SPEC=<path>: write the OpenAPI spec to the given file and exit without starting the server.
- SHIFTAPI_PORT=<port>: override the port in addr, allowing the Vite plugin to automatically assign a free port.
func Options ¶
func Options[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
Options registers an OPTIONS handler.
func Patch ¶
func Patch[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
Patch registers a PATCH handler.
func Post ¶
func Post[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
Post registers a handler for POST requests at the given path. The request body is automatically decoded from JSON (or multipart/form-data if the In type has form-tagged fields). Validation is applied before the handler runs.
Example ¶
package main
import (
"net/http"
"github.com/fcjr/shiftapi"
)
func main() {
api := shiftapi.New()
type CreateInput struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
}
type CreateOutput struct {
ID int `json:"id"`
}
shiftapi.Post(api, "/users", func(r *http.Request, in CreateInput) (*CreateOutput, error) {
return &CreateOutput{ID: 1}, nil
}, shiftapi.WithStatus(http.StatusCreated))
}
Output:
Example (FileUpload) ¶
package main
import (
"mime/multipart"
"net/http"
"github.com/fcjr/shiftapi"
)
func main() {
api := shiftapi.New()
type UploadInput struct {
File *multipart.FileHeader `form:"file" validate:"required"`
}
type UploadResult struct {
Filename string `json:"filename"`
Size int64 `json:"size"`
}
shiftapi.Post(api, "/upload", func(r *http.Request, in UploadInput) (*UploadResult, error) {
return &UploadResult{
Filename: in.File.Filename,
Size: in.File.Size,
}, nil
})
}
Output:
Example (QueryAndBody) ¶
package main
import (
"net/http"
"github.com/fcjr/shiftapi"
)
func main() {
api := shiftapi.New()
type Request struct {
Version string `query:"v"`
Name string `json:"name"`
}
type Response struct {
Result string `json:"result"`
}
shiftapi.Post(api, "/action", func(r *http.Request, in Request) (*Response, error) {
return &Response{Result: in.Name + " (v" + in.Version + ")"}, nil
})
_ = api
}
Output:
func Put ¶
func Put[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
Put registers a PUT handler.
func Trace ¶
func Trace[In, Resp any](api *API, path string, fn HandlerFunc[In, Resp], options ...RouteOption)
Trace registers a TRACE handler.
Types ¶
type API ¶
type API struct {
// contains filtered or unexported fields
}
API is the central type that collects typed handler registrations, generates an OpenAPI 3.1 schema, and implements http.Handler. Create one with New and register routes with Get, Post, Put, Patch, Delete, etc.
API automatically serves the OpenAPI spec at GET /openapi.json and interactive documentation at GET /docs.
func New ¶
New creates a new API with the given options. By default the API uses a 32 MB upload limit and the standard github.com/go-playground/validator/v10 instance. Use WithInfo, WithMaxUploadSize, WithValidator, and WithExternalDocs to customize behavior.
Example ¶
package main
import (
"github.com/fcjr/shiftapi"
)
func main() {
api := shiftapi.New(
shiftapi.WithInfo(shiftapi.Info{
Title: "Pet Store",
Version: "2.0.0",
Description: "A sample pet store API",
}),
shiftapi.WithMaxUploadSize(10<<20), // 10 MB
)
_ = api
}
Output:
func (*API) ServeHTTP ¶
func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request)
ServeHTTP implements http.Handler.
Example ¶
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/fcjr/shiftapi"
)
func main() {
api := shiftapi.New()
shiftapi.Get(api, "/ping", func(r *http.Request, _ struct{}) (*struct {
Pong bool `json:"pong"`
}, error) {
return &struct {
Pong bool `json:"pong"`
}{Pong: true}, nil
})
// Use as http.Handler in tests.
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/ping", nil)
api.ServeHTTP(w, r)
fmt.Println(w.Body.String())
}
Output: {"pong":true}
type APIError ¶
APIError is an error with an HTTP status code. Return it from handlers to control the response status code and message. Unrecognized errors (i.e. errors that are not *APIError or *ValidationError) are mapped to 500 Internal Server Error to prevent leaking implementation details.
func Error ¶
Error creates a new APIError with the given HTTP status code and message.
return nil, shiftapi.Error(http.StatusNotFound, "user not found")
Example ¶
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/fcjr/shiftapi"
)
func main() {
api := shiftapi.New()
type Empty struct{}
shiftapi.Get(api, "/secret", func(r *http.Request, _ struct{}) (*Empty, error) {
token := r.Header.Get("Authorization")
if token == "" {
return nil, shiftapi.Error(http.StatusUnauthorized, "missing auth token")
}
return &Empty{}, nil
})
// Make a request without auth to verify.
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/secret", nil)
api.ServeHTTP(w, r)
fmt.Println(w.Code)
fmt.Println(w.Body.String())
}
Output: 401 {"message":"missing auth token"}
type ExternalDocs ¶
ExternalDocs links to external documentation.
type FieldError ¶
FieldError describes a single field validation failure.
type HandlerFunc ¶
HandlerFunc is a typed handler function for API routes. The type parameters In and Resp are the request and response types — both are automatically reflected into the OpenAPI schema.
The In struct's fields are discriminated by struct tags:
- query:"name" — parsed from URL query parameters
- json:"name" — parsed from the JSON request body (default for POST/PUT/PATCH)
- form:"name" — parsed from multipart/form-data (for file uploads)
Use struct{} as In for routes that take no input, or as Resp for routes that return no body (e.g. health checks that only need a status code).
The *http.Request parameter gives access to headers, cookies, path parameters, and other request metadata.
type Info ¶
type Info struct {
Title string
Description string
TermsOfService string
Contact *Contact
License *License
Version string
}
Info describes the API and is rendered into the OpenAPI spec's info object.
type Option ¶
type Option func(*API)
Option configures an API created with New.
func WithExternalDocs ¶
func WithExternalDocs(docs ExternalDocs) Option
WithExternalDocs links to external documentation.
func WithInfo ¶
WithInfo configures the API metadata that appears in the OpenAPI spec and documentation UI.
api := shiftapi.New(shiftapi.WithInfo(shiftapi.Info{
Title: "My API",
Version: "1.0.0",
}))
func WithMaxUploadSize ¶ added in v0.0.24
WithMaxUploadSize sets the maximum memory used for parsing multipart form data. The default is 32 MB.
func WithValidator ¶
WithValidator sets a custom github.com/go-playground/validator/v10 instance on the API. Use this to register custom validations or override default behavior.
type RouteInfo ¶
RouteInfo provides metadata for a route that appears in the OpenAPI spec and the generated documentation UI.
type RouteOption ¶
type RouteOption func(*routeConfig)
RouteOption configures a route.
func WithRouteInfo ¶
func WithRouteInfo(info RouteInfo) RouteOption
WithRouteInfo sets the route's OpenAPI metadata (summary, description, tags).
shiftapi.Post(api, "/greet", greet, shiftapi.WithRouteInfo(shiftapi.RouteInfo{
Summary: "Greet a person",
Tags: []string{"greetings"},
}))
Example ¶
package main
import (
"net/http"
"github.com/fcjr/shiftapi"
)
func main() {
api := shiftapi.New()
shiftapi.Get(api, "/health", func(r *http.Request, _ struct{}) (*struct {
OK bool `json:"ok"`
}, error) {
return &struct {
OK bool `json:"ok"`
}{OK: true}, nil
}, shiftapi.WithRouteInfo(shiftapi.RouteInfo{
Summary: "Health check",
Description: "Returns the health status of the service.",
Tags: []string{"monitoring"},
}))
}
Output:
func WithStatus ¶
func WithStatus(status int) RouteOption
WithStatus sets the success HTTP status code for the route (default: 200). Use this for routes that should return 201 Created, 204 No Content, etc.
Example ¶
package main
import (
"net/http"
"github.com/fcjr/shiftapi"
)
func main() {
api := shiftapi.New()
type Item struct {
Name string `json:"name"`
}
type Created struct {
ID int `json:"id"`
}
shiftapi.Post(api, "/items", func(r *http.Request, in Item) (*Created, error) {
return &Created{ID: 1}, nil
}, shiftapi.WithStatus(http.StatusCreated))
_ = api
}
Output:
type ValidationError ¶
type ValidationError struct {
Message string `json:"message"`
Errors []FieldError `json:"errors"`
}
ValidationError is returned when request validation fails. It is serialized as a 422 Unprocessable Entity response with a structured list of per-field errors. Validation rules are specified using validate struct tags from github.com/go-playground/validator/v10 and are also reflected into the generated OpenAPI schema.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string