Repository navigation
Releases: raven-clown/tack
Release list
v0.9.0
The release candidate for v1.0 and the public launch. The public API in this release is the API v1.0 freezes, unless feedback during the two-week window (until 2026-10-25) shows a reason to change it. Every exported identifier was reviewed and kept (API_REVIEW.md); the three open questions are decided in DESIGN.md D11; MIGRATION.md has the before and after code for the one breaking change.
Breaking
- The
ShutdownTimeoutconstant is replaced byConfig.ShutdownTimeout, whose zero value keeps the ten seconds (DESIGN.md D11.1). Migration: delete references totack.ShutdownTimeout, or settack.Config{ShutdownTimeout: 10 * time.Second}where the value was read.
Security
- Every response tack writes (JSON, problems, the document, the docs page, and event streams) now carries
X-Content-Type-Options: nosniff; the docs page also sendsReferrer-Policy: no-referrer. The shared header values cost no allocation.
Added
bench/, its own module: the same pet API on net/http, Gin, Echo, Huma, and tack with one harness, an equivalence test that proves every variant enforces the same rules, andbench/run.shto rerun it. Results, including every case where tack is slower, are in BENCHMARKS.md.SECURITY.mdgains a "Defaults" section: body limits, content types, hidden internal errors, panics, headers, the timeoutsRunsets, shutdown, docs exposure, and what CI scans.FUTURE_FIT.mdandinternal/futurefit: the planned add-ons (API versioning, typed pagination, WebSocket, tackdb, tackcache, tackauth) written against the v0.9 API as compiling, running sketches. All six fit without changing an exported identifier; the extension points they would like are listed as additive (DESIGN.md D12).
v0.6.3
A hardening release: no public API changes.
Fixed
- Compiled validate checks now agree with the validator in every case a review found: a non-nil pointer counts as present whatever it points to (
*boolfalse withrequiredpasses,*intzero withomitempty,gte=1fails), bounds are parsed the way the validator parses them (float32at 32 bits, integers asint64,oneofoptions compared as the validator spells the value),requiredoromitemptyafter another rule keeps the validator's order, and a rule on theBodyfield or on ajson:"-"field no longer vanishes: such inputs run the validator. - A
validaterule the validator does not know inside a body struct panics at registration with the route and the rule, instead of on the first request. formfields on a GET or DELETE route, whichnet/httpnever reads, are refused at registration.- A stream handler behind a
ResponseWriterthat cannot flush gets a 500 instead of a silent empty 200; the message names theUnwrapmethod the middleware needs. A stream item that only embeds or points totack.Eventis rejected at registration instead of panicking. - A response body that cannot be encoded sends the 500 problem without the handler's headers, and with
Config.Deva successStatusthe route does not document logs a warning. App.Servereturns nil, as documented, when the caller shuts the server down itself.- A pointer field with
validate:"required"is no longer documented as nullable in the OpenAPI document, the TypeScript client, or the Zod schemas, since the server rejectsnull. tsgenrefuses a component schema named like a global the client relies on (Response,Error,Headers,Blob,FormData,Promise,Record, and the rest), with the fix, instead of generating a file that does not type-check.- Zod date-time schemas accept offsets (
.datetime({ offset: true })), which Go writes for non-UTC times; regenerateschemas.ts. tack mockreports a pathServeMuxcannot register, such as/files/{name}.json, instead of crashing; its preflight answer allows the headers the browser asked for, soAuthorizationworks; example sizes fromminLengthandminItemsare capped. The schema decoder tolerates anullentry inanyOf.tack devkills the whole process tree of the command after--, so a server started throughgo runno longer survives a restart and holds the port; a file that disappears between the directory listing and the stat no longer ends the loop;tack gen ts -hprints the usage.- The CodeQL finding in
examples/auth: the demo password comes fromAUTH_PASSWORDwith a documented default and is no longer logged.
Changed
- Request bodies are read into pooled buffers and results are encoded through a pooled pointer, so the encoder no longer copies every result; problem responses use the same pool and a shared
Content-Typevalue. On the machine that measured it, a bound route went from 2 allocations to 1, a JSON body route from 5 to 2, and a validation error from 10 to 8; see BENCHMARKS.md.TestAllocationsholds each shape to its budget. - The compiled checks live in
internal/bindnext to the plan;cmd/tackhas one file per subcommand. The public API is unchanged. - Security scanning: the Security workflow runs gosec on the library and on
cmd/separately, excluding for the command only the path, permission, subprocess, and request rules a CLI cannot satisfy; golangci-lint runs gosec with the same split; the two findings in the library are annotated with their reasons. Scorecard's open items are the ones a one-day-old single-maintainer project cannot close yet. - Tests: six fuzz targets (query scanner against
url.ParseQuery, JSON and form binding, header lists, path patterns, OpenAPI path helpers) run for 20 seconds each in CI; the README's Go blocks are compiled against the checkout;goleakguards the stream, server, andtack devtests. API_REVIEW.mdlists every exported identifier with a verdict for the v0.9 release candidate. Nothing changes in this release.
v0.6.2
Changed
- Routes whose
validatetags are all simple rules on scalar parameters (required,omitempty,min,max,gte,lte,gt,lt,len,eq,oneof) run compiled checks instead of the validator; bodies with rules, slices, and other rules keep the validator, and both produce the same errors. TheContent-Typeheader is set without canonicalizing or allocating, and inputs with nothing to bind skip the input pool. An empty handler now allocates nothing and costs less than plainnet/httpsetting its own header; a route with a path value, a query value, a default, and validation takes about 650 ns and one allocation, down from 1690 ns and ten in v0.6.0. See BENCHMARKS.md.
v0.6.1
Changed
- The request path allocates less: the query string is scanned for the bound fields instead of parsed into a map, route inputs are pooled, and responses are encoded by a pooled encoder. A route with a path value, a query value, a default, and validation went from 10 allocations and 504 bytes per request to 4 and 48, about a third faster; see BENCHMARKS.md. Behavior is unchanged, and malformed query pairs are skipped the way
url.ParseQueryskips them. - The benchmarks write to a discarding
ResponseWriter, so their numbers now measure the handlers rather thanhttptest.ResponseRecorder.
Fixed
- Two tests that failed on Go 1.27 and in CI: one depended on the exact text of a
jsonerror, andtack new's test disabled the module proxy, which the generated project'sgo mod tidyneeds.
v0.6.0
Added
- File uploads: a
form:"name"tag binds a field from amultipart/form-dataorapplication/x-www-form-urlencodedbody, and a*multipart.FileHeaderor[]*multipart.FileHeaderfield receives uploaded files. Form values use the same converters, defaults, andvalidaterules as query values, with locations such asform.photo. The request body is documented as a form with files as binary strings, so the TypeScript client sendsFormData(Blobfields), the Zod schemas usez.instanceof(Blob), and/docsshows a file picker. Temporary files are removed when the handler returns. An input cannot have both aBodyfield and form fields. - Server-sent events: a handler that returns
tack.Stream[T], an iteratorfunc(yield func(T) bool), sends one event per value with its JSON on the data line, flushing each.tack.Event[T]adds the event name and ID. The request context is done when the client disconnects or the server shuts down, andyieldreturns false once the connection is gone. The response is documented astext/event-streamwith the schema ofT; the client returnsAsyncIterable<ServerEvent<T>>andtack mocksends one example event. App.Run(ctx, addr)andApp.Serve(ctx, srv)serve until the context is done, then shut down gracefully: no new connections, every request context canceled (which ends streams), and up toShutdownTimeout(10 seconds) for in-flight handlers.Runuses a server with a 10 second header timeout and a two minute idle timeout and no body timeouts, so uploads and streams are not cut off.- A request whose
Content-LengthexceedsConfig.MaxBodyBytesis refused with 413 before the body is read. tack new <dir> [-module path]creates a runnable project: one resource with a typed error, groups, aLocationheader, graceful shutdown, a README, and a test that regenerates and checksclient/api.tsandclient/schemas.ts.tack versionprints the version. Publishing a GitHub release builds the command for Linux, macOS, and Windows (amd64 and arm64) and attaches the archives and a checksum file.examples/petstoregainsPOST /pets/{id}/photoandGET /pets/events, and usesRun.
Changed
- The detail of the
unsupported_media_type(415) problem is now "unsupported content type", since bodies can be JSON or forms.
v0.5.0
Added
tsgen.GenerateZod(doc)writes Zod schemas: one per component schema and one per operation input, with the bounds, lengths, patterns, enums, formats, and defaults the server enforces, so a form can validate before it sends. Each schema exports a value and a type of the same name; schemas in a reference cycle usez.lazywith an explicit type. The file importszodand works with Zod 3.23 and later, Zod 4 included.tsgen.CheckZodkeeps it fresh likeCheckdoes for the client.tack gen zod -spec <file|url|-> [-o schemas.ts]andtack check -spec <file|url|-> -zod schemas.ts.tack dev [-spec URL] [-o api.ts] [-zod schemas.ts] [-dir .] [-- command...]builds the directory withgo build, runs the binary, waits for the document URL to answer, and regenerates the client and schemas, writing only when they changed. A change to a Go file,go.mod, orgo.sumdoes it again; a failed build keeps the running app and reports the error. A command after--replaces the build and run step.tack mock -spec <file|url|-> [-addr 127.0.0.1:8081]serves every operation with its success status and a body built from the response schema (examples and defaults first, then values derived from the type, format, bounds, and enum). APrefer: code=404header returns that documented error with itscodeand detail. Responses allow cross-origin requests and the document is served at/openapi.json.examples/petstorecommitsclient/schemas.tsnext to its client; CI checks both withtack checkand type-checks them.
v0.4.0
Added
tsgen:Generate(doc)turns the OpenAPI document into one dependency-free TypeScript file built onfetch. Every operation is a method onClientthat returns aResult:{ ok: true, data, headers, response }with the response type and the documented headers parsed, or{ ok: false, error, response }whereerroris the union of the problem schemas the route declares, soswitch (res.error.code)is checked by the compiler. Responses the document does not describe are thrown asApiError, andunwrap(res)throws it for error results.ClientOptionstakesbaseUrl,fetch, defaultheaders, andcredentialsby security scheme name, which the client sends asAuthorization: Bearer,Authorization: Basic, or the API key header or query parameter the scheme names. Output is deterministic and sorted, so the file changes only when the API does.tsgen.Check(doc, path)returns an error wrappingtsgen.ErrStalewhen the file is missing or differs from the document, for a test that fails CI when the committed client is older than the routes.cmd/tack:tack gen ts -spec <file|url|-> [-o api.ts]writes the client andtack check -spec <file|url|-> api.tsexits 1 when it is stale. The document can come from a file, a URL such ashttp://localhost:8080/openapi.json, or standard input.- Generation fails with the fix when the document holds something the client cannot express: two operations with one ID, an ID that is not a TypeScript identifier, a schema named like something the client defines (
Client,Result,GetPetsErrorfor operationgetPets), two parameters with one name in different locations, a request body that is not JSON, or several success responses. examples/petstorecommits its generatedclient/api.tsand a test that fails when it is stale (go test . -updateregenerates it). CI checks the same file withtack checkfrom the served document and runs the client end to end with node.
v0.3.0
tack v0.3.0 adds route groups, middleware at three levels, typed errors that reach the docs by code, auth schemes in /docs, and responses with headers.
Added
- Route groups:
App.Group(prefix, mw...), nestable withGroup.Group, plusGroup.UseandGroup.With(opts...)to share route options. A prefix may hold wildcards such as/orgs/{org}. - Middleware at three levels with plain
func(http.Handler) http.Handler:App.Use, group middleware, and theMiddlewareroute option. Order: app, outer group, inner group, route, handler. chi'sRequestID,Logger, andRecovererwork unchanged. ContextValue[T](ctx, key)reads typed values that middleware stored in the context.- Typed errors:
DefineError(status, code, detail)returns an*Errorwith aCode;Error.WithDetailmakes a per-request copy,Error.Ismatches by code and status, andError.Unwrapreturns the handler's original error. TheErrors(...)route option lists them in the OpenAPI document, one<Code>Problemschema per code, so clients can build a typed union per route. WithConfig.Dev, returning an undeclared typed error logs a warning. Config.ErrorHandlerreplaces the error format for the whole app, andApp.WriteErrorlets middleware answer in that format.- Security schemes:
Config.SecuritywithBearer,Basic, andAPIKey, bound to routes or groups withSecurity(name, scopes...). They appear incomponents.securitySchemesand each operation'ssecurity, so/docsoffers a token field and sends it. Secured routes document a 401, andErrUnauthorizedis the matching error. - Responses with headers: an output struct with a
Bodyfield, an optionalStatus int, andheader-tagged fields, all documented in the spec. Registration panics when a struct hasheaderfields or an untaggedStatus intbut noBody. openapigainsSecurityScheme,SecurityRequirement,Header,Components.SecuritySchemes,Operation.Security,Response.Headers, andSchema.AllOf/OneOf.examples/auth: a login that signs a JWT and a route that requires it, as its own module.
Changed
- Every error tack sends now carries a
code:malformed_request(400),body_too_large(413),unsupported_media_type(415),validation_failed(422), andinternal(500). TheProblemDetailsschema gains the optionalcodeproperty. - An output struct with a field named
Bodyis now treated as a response wrapper, so onlyBodyis encoded. Rename the field if it was meant as a JSON property. examples/petstoreuses a group,DefineError, and aLocationheader.
Full changelog: https://github.com/raven-clown/tack/blob/main/CHANGELOG.md
v0.2.0
tack v0.2.0 generates an OpenAPI 3.1 document and an interactive docs page from the same structs that bind and validate requests.
Added
/openapi.jsonand/docs, configured withConfig.Description,Servers,SpecPath, andDocsPath;App.OpenAPIreturns the document.- Route options
Summary,Description,Tags,OperationID, andDeprecated. Operation IDs are generated from the method and path, such asgetPetsById. - JSON Schema 2020-12 for structs, embedded structs, slices, arrays, maps, pointers,
time.Time,[]byte,json.RawMessage, recursive types, and generics.doc,example, andvalidatetags map to schema keywords, andopenapi.SchemaProviderlets a type define its own schema. - Every operation documents its problem+json error responses through the
ProblemDetailsschema. - Request bodies with a non-JSON
Content-Typeget 415 Unsupported Media Type. - CI generates Go and TypeScript clients from the petstore spec with openapi-generator.
Changed
- A
nullrequest body counts as missing: 422 when the body is required, nil whenBodyis a pointer. - Errors implementing
HTTPStatus()send only their own message to the client. - Validation responses list at most 50 errors.
- Slice header fields also accept comma-separated values.
- Registration panics, naming the route and the fix, for invalid or duplicate paths, slice fields whose element rules need
validate:"dive",defaulttags inside JSON bodies, and success statuses that cannot carry the handler's output.
Fixed
- Validation error locations for anonymous and generic input types.
Full changelog: https://github.com/raven-clown/tack/blob/main/CHANGELOG.md
v0.1.0
tack v0.1.0 is the first usable version: typed routes on net/http with request binding, validation, and RFC 9457 errors.
Added
New,Config, andApp, anhttp.Handlerbuilt onhttp.ServeMux.- Typed routes
Get,Post,Put,Patch, andDeletewith inferred input and output types, and theStatusroute option. - Binding from
path,query,header, andcookietags and a JSONBodyfield, withdefaultvalues. Supports strings, bools, all integer and float types,time.Time,encoding.TextUnmarshaler, pointers for optional values, and slices for repeated values. - Validation with go-playground/validator, reporting every failing field at once with locations such as
body.nameandquery.page. - RFC 9457
application/problem+jsonerrors:Error,ErrorDetail,NewError, and theHTTPStatus() intconvention for domain errors. Request bodies are limited to 1 MiB by default. - Registration panics that name the route, the field, and the fix when an input struct does not match its path.
examples/petstoreandBENCHMARKS.md.
Full changelog: https://github.com/raven-clown/tack/blob/main/CHANGELOG.md