{"id":48246250,"url":"https://github.com/fcjr/shiftapi","last_synced_at":"2026-04-04T20:37:45.758Z","repository":{"id":87534557,"uuid":"420478784","full_name":"fcjr/shiftapi","owner":"fcjr","description":"Full-stack type-safety from go to typescript with OpenAPI schema generation out of the box.","archived":false,"fork":false,"pushed_at":"2026-03-12T04:08:03.000Z","size":644,"stargazers_count":7,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-12T04:40:44.541Z","etag":null,"topics":["framework","go","http","openapi","router","server"],"latest_commit_sha":null,"homepage":"https://www.shiftapi.dev","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fcjr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2021-10-23T17:28:20.000Z","updated_at":"2026-03-12T04:08:05.000Z","dependencies_parsed_at":"2023-07-27T06:45:48.104Z","dependency_job_id":null,"html_url":"https://github.com/fcjr/shiftapi","commit_stats":null,"previous_names":[],"tags_count":23,"template":false,"template_full_name":null,"purl":"pkg:github/fcjr/shiftapi","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fcjr%2Fshiftapi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fcjr%2Fshiftapi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fcjr%2Fshiftapi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fcjr%2Fshiftapi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fcjr","download_url":"https://codeload.github.com/fcjr/shiftapi/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fcjr%2Fshiftapi/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31413280,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T20:09:54.854Z","status":"ssl_error","status_checked_at":"2026-04-04T20:09:44.350Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["framework","go","http","openapi","router","server"],"created_at":"2026-04-04T20:37:44.754Z","updated_at":"2026-04-04T20:37:45.725Z","avatar_url":"https://github.com/fcjr.png","language":"Go","readme":"\n\n\u003cp align=\"center\"\u003e\n\t\u003cpicture\u003e\n\t\t\u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"assets/logo-dark.svg\"\u003e\n\t\t\u003cimg src=\"assets/logo.svg\" alt=\"ShiftAPI Logo\"\u003e\n\t\u003c/picture\u003e\n\u003c/p\u003e\n\n\u003ch3 align=\"center\"\u003eEnd-to-end type safety from Go structs to TypeScript frontend.\u003c/h3\u003e\n\n\u003cp align=\"center\"\u003e\n  ShiftAPI is a Go framework that generates an OpenAPI 3.1 spec from your handler types at runtime, then uses a Vite or Next.js plugin to turn that spec into a fully-typed TypeScript client — so your frontend stays in sync with your API automatically.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://pkg.go.dev/github.com/fcjr/shiftapi\"\u003e\u003cimg src=\"https://pkg.go.dev/badge/github.com/fcjr/shiftapi.svg\" alt=\"Go Reference\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/fcjr/shiftapi/actions?query=workflow%3Ago-lint\"\u003e\u003cimg src=\"https://github.com/fcjr/shiftapi/workflows/ci/badge.svg\" alt=\"GolangCI\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://goreportcard.com/report/github.com/fcjr/shiftapi\"\u003e\u003cimg src=\"https://goreportcard.com/badge/github.com/fcjr/shiftapi\" alt=\"Go Report Card\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/shiftapi\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/shiftapi?label=shiftapi\" alt=\"npm shiftapi\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@shiftapi/vite-plugin\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/@shiftapi/vite-plugin?label=%40shiftapi%2Fvite-plugin\" alt=\"npm @shiftapi/vite-plugin\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@shiftapi/next\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/@shiftapi/next?label=%40shiftapi%2Fnext\" alt=\"npm @shiftapi/next\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n```\nGo structs ──→ OpenAPI 3.1 spec ──→ TypeScript types ──→ Typed fetch client\n   (compile time)     (runtime)         (build time)        (your frontend)\n```\n\n## Getting Started\n\nScaffold a full-stack app (Go + React, Svelte, or Next.js):\n\n```sh\nnpm create shiftapi@latest\n```\n\nOr add ShiftAPI to an existing Go project:\n\n```sh\ngo get github.com/fcjr/shiftapi\n```\n\n## Quick Start\n\n```go\npackage main\n\nimport (\n    \"log\"\n    \"net/http\"\n\n    \"github.com/fcjr/shiftapi\"\n)\n\ntype Person struct {\n    Name string `json:\"name\" validate:\"required\"`\n}\n\ntype Greeting struct {\n    Hello string `json:\"hello\"`\n}\n\nfunc greet(r *http.Request, in *Person) (*Greeting, error) {\n    return \u0026Greeting{Hello: in.Name}, nil\n}\n\nfunc main() {\n    api := shiftapi.New(shiftapi.WithInfo(shiftapi.Info{\n        Title:   \"Greeter API\",\n        Version: \"1.0.0\",\n    }))\n\n    shiftapi.Handle(api, \"POST /greet\", greet)\n\n    log.Println(\"listening on :8080\")\n    log.Fatal(shiftapi.ListenAndServe(\":8080\", api))\n    // interactive docs at http://localhost:8080/docs\n}\n```\n\nThat's it. ShiftAPI reflects your Go types into an OpenAPI 3.1 spec at `/openapi.json` and serves interactive docs at `/docs` — no code generation step, no annotations.\n\n## Features\n\n### Generic type-safe handlers\n\nGeneric free functions capture your request and response types at compile time. Every method uses a single function — struct tags discriminate query params (`query:\"...\"`), HTTP headers (`header:\"...\"`), body fields (`json:\"...\"`), and form fields (`form:\"...\"`). For routes without input, use `_ struct{}`.\n\n```go\n// POST with body — input is decoded and passed as *CreateUser\nshiftapi.Handle(api, \"POST /users\", func(r *http.Request, in *CreateUser) (*User, error) {\n    return db.CreateUser(r.Context(), in)\n}, shiftapi.WithStatus(http.StatusCreated))\n\n// GET without input — use _ struct{}\nshiftapi.Handle(api, \"GET /users/{id}\", func(r *http.Request, _ struct{}) (*User, error) {\n    return db.GetUser(r.Context(), r.PathValue(\"id\"))\n})\n```\n\n### Typed path parameters\n\nUse `path` tags to declare typed path parameters. They are parsed from the URL, validated, and documented in the OpenAPI spec automatically:\n\n```go\ntype GetUserInput struct {\n    ID int `path:\"id\" validate:\"required,gt=0\"`\n}\n\nshiftapi.Handle(api, \"GET /users/{id}\", func(r *http.Request, in GetUserInput) (*User, error) {\n    return db.GetUser(r.Context(), in.ID) // in.ID is already an int\n})\n```\n\nSupports the same scalar types as query params: `string`, `bool`, `int*`, `uint*`, `float*`. Use `validate:\"uuid\"` on a `string` field for UUID path params. Parse errors return `400`; validation failures return `422`.\n\nPath parameters are always required and always scalar — pointers and slices on `path`-tagged fields panic at registration time. You can still use `r.PathValue(\"id\")` directly for routes that don't need typed path params.\n\n### Typed query parameters\n\nDefine a struct with `query` tags. Query params are parsed, validated, and documented in the OpenAPI spec automatically.\n\n```go\ntype SearchQuery struct {\n    Q     string `query:\"q\"     validate:\"required\"`\n    Page  int    `query:\"page\"  validate:\"min=1\"`\n    Limit int    `query:\"limit\" validate:\"min=1,max=100\"`\n}\n\nshiftapi.Handle(api, \"GET /search\", func(r *http.Request, in SearchQuery) (*Results, error) {\n    return doSearch(in.Q, in.Page, in.Limit), nil\n})\n```\n\nSupports `string`, `bool`, `int*`, `uint*`, `float*` scalars, `*T` pointers for optional params, and `[]T` slices for repeated params (e.g. `?tag=a\u0026tag=b`). Parse errors return `400`; validation failures return `422`.\n\nFor handlers that need both query parameters and a request body, combine them in a single struct — fields with `query` tags become query params, fields with `json` tags become the body:\n\n```go\ntype CreateInput struct {\n    DryRun bool   `query:\"dry_run\"`\n    Name   string `json:\"name\"`\n}\n\nshiftapi.Handle(api, \"POST /items\", func(r *http.Request, in CreateInput) (*Result, error) {\n    return createItem(in.Name, in.DryRun), nil\n})\n```\n\n### Typed HTTP headers\n\nDefine a struct with `header` tags. Headers are parsed, validated, and documented in the OpenAPI spec automatically — just like query params.\n\n```go\ntype AuthInput struct {\n    Token string `header:\"Authorization\" validate:\"required\"`\n    Q     string `query:\"q\"`\n}\n\nshiftapi.Handle(api, \"GET /search\", func(r *http.Request, in AuthInput) (*Results, error) {\n    // in.Token parsed from the Authorization header\n    // in.Q parsed from ?q= query param\n    return doSearch(in.Token, in.Q), nil\n})\n```\n\nSupports `string`, `bool`, `int*`, `uint*`, `float*` scalars and `*T` pointers for optional headers. Parse errors return `400`; validation failures return `422`. Header, query, and body fields can be freely combined in one struct.\n\n### File uploads (`multipart/form-data`)\n\nUse `form` tags to declare file upload endpoints. The `form` tag drives OpenAPI spec generation — the generated TypeScript client gets the correct `multipart/form-data` types automatically. At runtime, the request body is parsed via `ParseMultipartForm` and form-tagged fields are populated.\n\n```go\ntype UploadInput struct {\n    File  *multipart.FileHeader   `form:\"file\" validate:\"required\"`\n    Title string                  `form:\"title\" validate:\"required\"`\n    Tags  string                  `query:\"tags\"`\n}\n\nshiftapi.Handle(api, \"POST /upload\", func(r *http.Request, in UploadInput) (*Result, error) {\n    f, err := in.File.Open()\n    if err != nil {\n        return nil, fmt.Errorf(\"failed to open file: %w\", err)\n    }\n    defer f.Close()\n    // read from f, save to disk/S3/etc.\n    return \u0026Result{Filename: in.File.Filename, Title: in.Title}, nil\n})\n```\n\n- `*multipart.FileHeader` — single file (`type: string, format: binary` in OpenAPI, `File | Blob | Uint8Array` in TypeScript)\n- `[]*multipart.FileHeader` — multiple files (`type: array, items: {type: string, format: binary}`)\n- Scalar types with `form` tag — text form fields\n- `query` tags work alongside `form` tags\n- Mixing `json` and `form` tags on the same struct panics at registration time\n\nRestrict accepted file types with the `accept` tag. This validates the `Content-Type` at runtime (returns `400` if rejected) and documents the constraint in the OpenAPI spec via the `encoding` map:\n\n```go\ntype ImageUpload struct {\n    Avatar *multipart.FileHeader `form:\"avatar\" accept:\"image/png,image/jpeg\" validate:\"required\"`\n}\n```\n\nThe default max upload size is 32 MB. Configure it with `WithMaxUploadSize`:\n\n```go\napi := shiftapi.New(shiftapi.WithMaxUploadSize(64 \u003c\u003c 20)) // 64 MB\n```\n\n### Validation\n\nBuilt-in validation via [go-playground/validator](https://github.com/go-playground/validator). Struct tags are enforced at runtime *and* reflected into the OpenAPI schema.\n\n```go\ntype CreateUser struct {\n    Name  string `json:\"name\"  validate:\"required,min=2,max=50\"`\n    Email string `json:\"email\" validate:\"required,email\"`\n    Age   int    `json:\"age\"   validate:\"gte=0,lte=150\"`\n    Role  string `json:\"role\"  validate:\"oneof=admin user guest\"`\n}\n```\n\nInvalid requests return `422` with per-field errors:\n\n```json\n{\n    \"message\": \"validation failed\",\n    \"errors\": [\n        { \"field\": \"Name\",  \"message\": \"this field is required\" },\n        { \"field\": \"Email\", \"message\": \"must be a valid email address\" }\n    ]\n}\n```\n\nSupported tags: `required`, `email`, `url`/`uri`, `uuid`, `datetime`, `min`, `max`, `gte`, `lte`, `gt`, `lt`, `len`, `oneof` — all mapped to their OpenAPI equivalents (`format`, `minimum`, `maxLength`, `enum`, etc.). Use `WithValidator()` to supply a custom validator instance.\n\n### Route groups\n\nUse `Group` to create a sub-router with a shared path prefix and options. Groups can be nested:\n\n```go\nv1 := api.Group(\"/api/v1\",\n    shiftapi.WithError[*RateLimitError](http.StatusTooManyRequests),\n    shiftapi.WithMiddleware(auth),\n)\n\nshiftapi.Handle(v1, \"GET /users\", listUsers)   // GET /api/v1/users\nshiftapi.Handle(v1, \"POST /users\", createUser) // POST /api/v1/users\n\nadmin := v1.Group(\"/admin\",\n    shiftapi.WithError[*ForbiddenError](http.StatusForbidden),\n    shiftapi.WithMiddleware(adminOnly),\n)\nshiftapi.Handle(admin, \"GET /stats\", getStats) // GET /api/v1/admin/stats\n```\n\n### Middleware\n\nUse `WithMiddleware` to apply standard HTTP middleware at any level — API, group, or route:\n\n```go\napi := shiftapi.New(\n    shiftapi.WithMiddleware(cors, logging),          // all routes\n)\nv1 := api.Group(\"/api/v1\",\n    shiftapi.WithMiddleware(auth),                   // group routes\n)\nshiftapi.Handle(v1, \"GET /admin\", getAdmin,\n    shiftapi.WithMiddleware(adminOnly),               // single route\n)\n```\n\nMiddleware resolves from outermost to innermost: **API → parent Group → child Group → Route → handler**. Within a single `WithMiddleware(a, b)` call, the first argument wraps outermost.\n\n### Context values\n\nUse `NewContextKey`, `SetContext`, and `FromContext` to pass typed data from middleware to handlers — no untyped `context.Value` keys or type assertions needed:\n\n```go\nvar userKey = shiftapi.NewContextKey[User](\"user\")\n\n// Middleware stores the value:\nfunc authMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        user, err := authenticate(r)\n        if err != nil {\n            http.Error(w, \"unauthorized\", http.StatusUnauthorized)\n            return\n        }\n        next.ServeHTTP(w, shiftapi.SetContext(r, userKey, user))\n    })\n}\n\n// Handler retrieves it — fully typed, no assertion needed:\nshiftapi.Handle(authed, \"GET /me\", func(r *http.Request, _ struct{}) (*Profile, error) {\n    user, ok := shiftapi.FromContext(r, userKey)\n    if !ok {\n        return nil, fmt.Errorf(\"missing user context\")\n    }\n    return \u0026Profile{Name: user.Name}, nil\n})\n```\n\nEach `ContextKey` has pointer identity, so two keys for the same type never collide. The type parameter ensures `SetContext` and `FromContext` agree on the value type at compile time.\n\n### Error handling\n\nUse `WithError` to declare that a handler may return a specific error type at a given HTTP status code. Works at any level — API, group, or route:\n\n```go\napi := shiftapi.New(\n    shiftapi.WithError[*AuthError](http.StatusUnauthorized),         // all routes\n)\nshiftapi.Handle(api, \"GET /users/{id}\", getUser,\n    shiftapi.WithError[*NotFoundError](http.StatusNotFound),         // single route\n)\n```\n\nThe error type must implement `error` — its struct fields are reflected into the OpenAPI schema. At runtime, if the handler returns a matching error (via `errors.As`), it is serialized as JSON with the declared status code. Wrapped errors work automatically. Unrecognized errors return `500`.\n\nCustomize the default 400/500 responses with `WithBadRequestError` and `WithInternalServerError`:\n\n```go\napi := shiftapi.New(\n    shiftapi.WithBadRequestError(func(err error) *MyBadRequest {\n        return \u0026MyBadRequest{Code: \"BAD_REQUEST\", Message: err.Error()}\n    }),\n    shiftapi.WithInternalServerError(func(err error) *MyServerError {\n        log.Error(\"unhandled\", \"err\", err)\n        return \u0026MyServerError{Code: \"INTERNAL_ERROR\", Message: \"internal server error\"}\n    }),\n)\n```\n\nEvery route automatically includes `400`, `422` ([ValidationError](https://pkg.go.dev/github.com/fcjr/shiftapi#ValidationError)), and `500` responses in the generated OpenAPI spec.\n\n### Option composition\n\n`WithError` and `WithMiddleware` are `Option` values — they work at all three levels. Use `ComposeOptions` to bundle them into reusable options:\n\n```go\nfunc WithAuth() shiftapi.Option {\n    return shiftapi.ComposeOptions(\n        shiftapi.WithMiddleware(authMiddleware),\n        shiftapi.WithError[*AuthError](http.StatusUnauthorized),\n    )\n}\n```\n\nFor level-specific composition (mixing shared and level-specific options), use `ComposeAPIOptions`, `ComposeGroupOptions`, or `ComposeRouteOptions`:\n\n```go\ncreateOpts := shiftapi.ComposeRouteOptions(\n    shiftapi.WithStatus(http.StatusCreated),\n    shiftapi.WithError[*ConflictError](http.StatusConflict),\n)\nshiftapi.Handle(api, \"POST /users\", createUser, createOpts)\n```\n\n### Route metadata\n\nAdd OpenAPI summaries, descriptions, and tags per route:\n\n```go\nshiftapi.Handle(api, \"POST /greet\", greet,\n    shiftapi.WithRouteInfo(shiftapi.RouteInfo{\n        Summary:     \"Greet a person\",\n        Description: \"Returns a personalized greeting.\",\n        Tags:        []string{\"greetings\"},\n    }),\n)\n```\n\n### Standard `http.Handler`\n\n`API` implements `http.Handler`, so it works with any middleware, `httptest`, and `ServeMux` mounting:\n\n```go\n// middleware\nwrapped := loggingMiddleware(corsMiddleware(api))\nhttp.ListenAndServe(\":8080\", wrapped)\n\n// mount under a prefix\nmux := http.NewServeMux()\nmux.Handle(\"/api/v1/\", http.StripPrefix(\"/api/v1\", api))\n```\n\n## TypeScript Integration\n\nShiftAPI ships npm packages for the frontend:\n\n- **`shiftapi`** — CLI and codegen core. Extracts the OpenAPI spec from your Go server, generates TypeScript types via [openapi-typescript](https://github.com/openapi-ts/openapi-typescript), and writes a pre-configured [openapi-fetch](https://github.com/openapi-ts/openapi-typescript/tree/main/packages/openapi-fetch) client.\n- **`@shiftapi/vite-plugin`** — Vite plugin for dev-time HMR, proxy, and Go server management.\n- **`@shiftapi/next`** — Next.js integration with the same DX (webpack/Turbopack aliases, rewrites proxy, Go server management).\n\n**`shiftapi.config.ts`** (project root):\n\n```typescript\nimport { defineConfig } from \"shiftapi\";\n\nexport default defineConfig({\n    server: \"./cmd/server\", // Go entry point\n});\n```\n\n### Vite\n\n```sh\nnpm install shiftapi @shiftapi/vite-plugin\n```\n\n```typescript\n// vite.config.ts\nimport shiftapi from \"@shiftapi/vite-plugin\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig({\n    plugins: [shiftapi()],\n});\n```\n\n### Next.js\n\n```sh\nnpm install shiftapi @shiftapi/next\n```\n\n```typescript\n// next.config.ts\nimport type { NextConfig } from \"next\";\nimport { withShiftAPI } from \"@shiftapi/next\";\n\nconst nextConfig: NextConfig = {};\n\nexport default withShiftAPI(nextConfig);\n```\n\n### Use the typed client\n\n```typescript\nimport { client } from \"@shiftapi/client\";\n\nconst { data } = await client.GET(\"/health\");\n// data: { ok?: boolean }\n\nconst { data: greeting } = await client.POST(\"/greet\", {\n    body: { name: \"frank\" },\n});\n// body and response are fully typed from your Go structs\n\nconst { data: results } = await client.GET(\"/search\", {\n    params: { query: { q: \"hello\", page: 1, limit: 10 } },\n});\n// query params are fully typed too — { q: string, page?: number, limit?: number }\n\nconst { data: upload } = await client.POST(\"/upload\", {\n    body: { file: new File([\"content\"], \"doc.txt\"), title: \"My Doc\" },\n    params: { query: { tags: \"important\" } },\n});\n// file uploads are typed as File | Blob | Uint8Array — generated from format: binary in the spec\n\nconst { data: authResults } = await client.GET(\"/search\", {\n    params: {\n        query: { q: \"hello\" },\n        header: { Authorization: \"Bearer token\" },\n    },\n});\n// header params are fully typed as well\n```\n\nIn dev mode the plugins start the Go server, proxy API requests, watch `.go` files, and regenerate types on changes.\n\n**CLI usage (without Vite/Next.js):**\n\n```sh\nshiftapi prepare\n```\n\nThis extracts the spec and generates `.shiftapi/client.d.ts` and `.shiftapi/client.js`. Useful in `postinstall` scripts or CI.\n\n**Config options:**\n\n| Option | Default | Description |\n|---|---|---|\n| `server` | *(required)* | Go entry point (e.g. `\"./cmd/server\"`) |\n| `baseUrl` | `\"/\"` | Fallback base URL for the API client |\n| `url` | `\"http://localhost:8080\"` | Go server address for dev proxy |\n\nFor production, set `VITE_SHIFTAPI_BASE_URL` (Vite) or `NEXT_PUBLIC_SHIFTAPI_BASE_URL` (Next.js) to point at your API host. The plugins automatically update `tsconfig.json` with the required path mapping for IDE autocomplete.\n\n## Development\n\nThis is a pnpm + [Turborepo](https://turbo.build) monorepo.\n\n```bash\npnpm install    # install dependencies\npnpm build      # build all packages\npnpm dev        # start example Vite + Go app\npnpm test       # run all tests\n```\n\nGo tests can also be run directly:\n\n```bash\ngo test -count=1 -tags shiftapidev ./...\n```\n\n---\n\n\u003cp align=\"center\"\u003eMade with love for types at the \u003ca href=\"https://www.recurse.com\"\u003eRecurse Center\u003c/a\u003e\u003c/p\u003e\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffcjr%2Fshiftapi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffcjr%2Fshiftapi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffcjr%2Fshiftapi/lists"}