shiftapi

package module
v0.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 17, 2026 License: MIT Imports: 14 Imported by: 0

README

ShiftAPI Logo

ShiftAPI

Quickly write RESTful APIs in Go with automatic OpenAPI 3.1 schema generation.

Inspired by the simplicity of FastAPI.

GolangCI Go Report Card

Getting Started

The fastest way to get started is with create-shiftapi, which scaffolds a full-stack app with a Go API backend and a typed frontend (React or Svelte):

npx create-shiftapi@latest

You don't need a frontend to use ShiftAPI — it works great as a standalone Go API framework too. To add it to an existing Go project:

go get github.com/fcjr/shiftapi

Quick Start

package main

import (
    "log"
    "net/http"

    "github.com/fcjr/shiftapi"
)

type Person struct {
    Name string `json:"name" validate:"required"`
}

type Greeting struct {
    Hello string `json:"hello"`
}

func greet(r *http.Request, body *Person) (*Greeting, error) {
    return &Greeting{Hello: body.Name}, nil
}

func main() {
    api := shiftapi.New(shiftapi.WithInfo(shiftapi.Info{
        Title:       "Greeter API",
        Description: "It greets you by name.",
        Version:     "1.0.0",
    }))

    shiftapi.Post(api, "/greet", greet)

    log.Println("listening on :8080")
    log.Fatal(shiftapi.ListenAndServe(":8080", api))
    // docs at http://localhost:8080/docs
}

How It Works

ShiftAPI is a thin layer on top of net/http. The API type implements http.Handler, so it works with the standard library server, middleware, and testing tools.

Generic free functions (Get, Post, Put, etc.) capture your request/response types at compile time for two purposes:

  1. Type-safe request handling — request bodies are automatically decoded from JSON and passed to your handler as typed values.
  2. Automatic OpenAPI generation — the types are reflected into an OpenAPI 3.1 spec served at /openapi.json, with interactive docs at /docs.

Usage

Handlers with a request body (POST, PUT, PATCH)
shiftapi.Post(api, "/users", func(r *http.Request, body *CreateUser) (*User, error) {
    user, err := db.CreateUser(r.Context(), body)
    if err != nil {
        return nil, err
    }
    return user, nil
}, shiftapi.WithStatus(http.StatusCreated))
Handlers without a request body (GET, DELETE, HEAD)
shiftapi.Get(api, "/users/{id}", func(r *http.Request) (*User, error) {
    id := r.PathValue("id")  // standard Go 1.22+ path params
    return db.GetUser(r.Context(), id)
})

Since the handler receives a standard *http.Request, you have full access to path params, query params, headers, cookies, context — everything you'd have in a regular http.HandlerFunc.

Validation

ShiftAPI has built-in validation powered by go-playground/validator. Add validate struct tags to your request types — they are enforced at runtime and automatically reflected in the OpenAPI schema.

type CreateUser struct {
    Name  string `json:"name"  validate:"required,min=2,max=50"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age"   validate:"gte=0,lte=150"`
    Role  string `json:"role"  validate:"oneof=admin user guest"`
}

Invalid requests return a 422 Unprocessable Entity with per-field errors:

{
    "message": "validation failed",
    "errors": [
        { "field": "Name",  "message": "this field is required" },
        { "field": "Email", "message": "must be a valid email address" }
    ]
}

Supported tags: required, email, url/uri, uuid, datetime, min, max, gte, lte, gt, lt, len, oneof. All are mapped to the corresponding OpenAPI schema properties (format, minimum, maxLength, enum, etc.).

To use a custom validator instance:

api := shiftapi.New(shiftapi.WithValidator(myValidator))
Error Handling

Return shiftapi.Error to control the HTTP status code and message:

shiftapi.Get(api, "/users/{id}", func(r *http.Request) (*User, error) {
    user, err := db.GetUser(r.Context(), r.PathValue("id"))
    if err != nil {
        return nil, shiftapi.Error(http.StatusNotFound, "user not found")
    }
    return user, nil
})

Any non-APIError returns a 500 Internal Server Error. APIError responses are returned as JSON:

{"message": "user not found"}
Route Metadata

Add OpenAPI metadata with WithRouteInfo:

shiftapi.Post(api, "/greet", greet,
    shiftapi.WithRouteInfo(shiftapi.RouteInfo{
        Summary:     "Greet a person",
        Description: "Greet a person with a friendly greeting",
        Tags:        []string{"greetings"},
    }),
)
Middleware

Since API implements http.Handler, any standard middleware works:

api := shiftapi.New()
shiftapi.Get(api, "/health", healthHandler)

wrapped := loggingMiddleware(corsMiddleware(api))
http.ListenAndServe(":8080", wrapped)
Mounting Under a Prefix
api := shiftapi.New()
shiftapi.Get(api, "/health", healthHandler)

mux := http.NewServeMux()
mux.Handle("/api/v1/", http.StripPrefix("/api/v1", api))
http.ListenAndServe(":8080", mux)
TypeScript Type Safety

ShiftAPI can generate fully-typed TypeScript clients from your Go types using the @shiftapi/vite-plugin. One line change on the Go side, full autocomplete on the frontend.

Go — use shiftapi.ListenAndServe instead of http.ListenAndServe:

log.Fatal(shiftapi.ListenAndServe(":8080", api))

Install the Vite plugin:

npm install @shiftapi/vite-plugin

vite.config.ts:

import shiftapi from "@shiftapi/vite-plugin";
import { defineConfig } from "vite";

export default defineConfig({
    plugins: [
        shiftapi({
            server: "./cmd/server",  // Go entry point
        }),
    ],
});

Frontend — import the typed client:

import { client } from "@shiftapi/client";

const { data } = await client.GET("/health");
// data: { ok?: boolean }

const { data: greeting } = await client.POST("/greet", {
    body: { name: "frank" },
});
// body and response are fully typed from your Go structs

The plugin extracts your OpenAPI spec at build time (no running server required), generates TypeScript types via openapi-typescript, and serves a pre-configured openapi-fetch client as a virtual module.

In dev mode, the plugin also:

  • Starts the Go server automatically (go run)
  • Auto-configures Vite's proxy to forward API requests
  • Watches .go files — on change, restarts the server, regenerates types, and reloads the browser

Plugin options:

Option Default Description
server (required) Go entry point (e.g. "./cmd/server")
baseUrl "/" Fallback base URL for the API client
goRoot process.cwd() Go module root directory
url "http://localhost:8080" Go server address for dev proxy

Configuring the API base URL for production:

In dev mode, the plugin proxies all API requests through Vite's dev server so the default baseUrl of "/" works automatically. In production, where the API runs on a different host, set the VITE_SHIFTAPI_BASE_URL environment variable:

# .env.production
VITE_SHIFTAPI_BASE_URL=https://api.example.com

This follows Vite's standard env file convention — .env.production is loaded during vite build, .env.development during vite dev, etc. The baseUrl plugin option serves as the fallback when the env var is not set.

The plugin automatically updates your tsconfig.json with the required path mapping for IDE autocomplete on first run.

Testing

Use httptest directly:

func TestHealthEndpoint(t *testing.T) {
    api := shiftapi.New()
    shiftapi.Get(api, "/health", healthHandler)

    req := httptest.NewRequest(http.MethodGet, "/health", nil)
    rec := httptest.NewRecorder()
    api.ServeHTTP(rec, req)

    if rec.Code != http.StatusOK {
        t.Fatalf("expected 200, got %d", rec.Code)
    }
}
Development

This is a pnpm + Turborepo monorepo. Turbo handles the build dependency graph — running pnpm dev will automatically build the Vite plugin before starting the example app.

pnpm install    # install dependencies
pnpm build      # build all packages
pnpm dev        # build plugin, then start example Vite + Go app
pnpm test       # run all tests

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Connect

func Connect[Body, Resp any](api *API, path string, fn HandlerFuncWithBody[Body, Resp], options ...RouteOption)

Connect registers a CONNECT handler.

func Delete

func Delete[Resp any](api *API, path string, fn HandlerFunc[Resp], options ...RouteOption)

Delete registers a DELETE handler.

func Get

func Get[Resp any](api *API, path string, fn HandlerFunc[Resp], options ...RouteOption)

Get registers a GET handler.

func Head[Resp any](api *API, path string, fn HandlerFunc[Resp], options ...RouteOption)

Head registers a HEAD handler.

func ListenAndServe

func ListenAndServe(addr string, api *API) error

ListenAndServe starts the HTTP server on the given address.

If the SHIFTAPI_EXPORT_SPEC environment variable is set to a file path, the OpenAPI spec is written to that path and the process exits immediately without starting the server. This enables build tools (like the Vite plugin) to extract the spec by running the Go binary.

func Options

func Options[Resp any](api *API, path string, fn HandlerFunc[Resp], options ...RouteOption)

Options registers an OPTIONS handler.

func Patch

func Patch[Body, Resp any](api *API, path string, fn HandlerFuncWithBody[Body, Resp], options ...RouteOption)

Patch registers a PATCH handler.

func Post

func Post[Body, Resp any](api *API, path string, fn HandlerFuncWithBody[Body, Resp], options ...RouteOption)

Post registers a POST handler.

func Put

func Put[Body, Resp any](api *API, path string, fn HandlerFuncWithBody[Body, Resp], options ...RouteOption)

Put registers a PUT handler.

func Trace

func Trace[Resp any](api *API, path string, fn HandlerFunc[Resp], options ...RouteOption)

Trace registers a TRACE handler.

Types

type API

type API struct {
	// contains filtered or unexported fields
}

API collects typed handler registrations, generates an OpenAPI schema, and implements http.Handler so it can be used with any standard server.

func New

func New(options ...Option) *API

New creates a new API with the given options.

func (*API) ServeHTTP

func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler.

func (*API) Spec

func (a *API) Spec() *openapi3.T

Spec returns the OpenAPI specification.

type APIError

type APIError struct {
	Status  int    `json:"-"`
	Message string `json:"message"`
}

APIError is an error with an HTTP status code. Return it from handlers to control the response status code and message.

func Error

func Error(status int, message string) *APIError

Error creates a new APIError with the given status code and message.

func (*APIError) Error

func (e *APIError) Error() string

type Contact

type Contact struct {
	Name  string
	URL   string
	Email string
}

Contact describes the API contact information.

type ExternalDocs

type ExternalDocs struct {
	Description string
	URL         string
}

ExternalDocs links to external documentation.

type FieldError

type FieldError struct {
	Field   string `json:"field"`
	Message string `json:"message"`
}

FieldError describes a single field validation failure.

type HandlerFunc

type HandlerFunc[Resp any] func(r *http.Request) (Resp, error)

HandlerFunc is a typed handler for methods without a request body (GET, DELETE, HEAD, etc.).

type HandlerFuncWithBody

type HandlerFuncWithBody[Body, Resp any] func(r *http.Request, body Body) (Resp, error)

HandlerFuncWithBody is a typed handler for methods with a request body (POST, PUT, PATCH, etc.).

type Info

type Info struct {
	Title          string
	Summary        string
	Description    string
	TermsOfService string
	Contact        *Contact
	License        *License
	Version        string
}

Info describes the API.

type License

type License struct {
	Name       string
	URL        string
	Identifier string
}

License describes the API license.

type Option

type Option func(*API)

Option configures an API.

func WithExternalDocs

func WithExternalDocs(docs ExternalDocs) Option

WithExternalDocs links to external documentation.

func WithInfo

func WithInfo(info Info) Option

WithInfo configures the API metadata.

func WithValidator

func WithValidator(v *validator.Validate) Option

WithValidator sets a custom validator instance on the API.

type RouteInfo

type RouteInfo struct {
	Summary     string
	Description string
	Tags        []string
}

RouteInfo provides metadata for a route in the OpenAPI spec.

type RouteOption

type RouteOption func(*routeConfig)

RouteOption configures a route.

func WithRouteInfo

func WithRouteInfo(info RouteInfo) RouteOption

WithRouteInfo sets the route's OpenAPI metadata.

func WithStatus

func WithStatus(status int) RouteOption

WithStatus sets the success HTTP status code for the route (default: 200).

type ValidationError

type ValidationError struct {
	Message string       `json:"message"`
	Errors  []FieldError `json:"errors"`
}

ValidationError is returned when request body validation fails.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Directories

Path Synopsis
examples
greeter command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL