# go-tigertonic: a Dropwizard style JSON framework for Go

> Tiger Tonic is a Go framework for JSON web services that takes its shape from Dropwizard while keeping Go's preference for orthogonal pieces: routing, marshaling, logging, metrics, auth and CORS are separate wrappers around http.Handler. It is built for JSON APIs and says so bluntly, and its licence is a custom one rather than a standard open source grant.

**rcrowley/go-tigertonic** — A Go framework for building JSON web services inspired by Dropwizard

- Repository: https://github.com/rcrowley/go-tigertonic
- Stars: 987 · Forks: 74
- Language: Go
- License: NOASSERTION
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/rcrowley-go-tigertonic

## A Dropwizard shape that keeps Go features orthogonal

Tiger Tonic describes itself as a Go framework for building JSON web services inspired by Dropwizard, and it opens with a warning that sets the tone for the whole project: if HTML is your game, this will hurt a little.

That single sentence is an accurate summary of the scope. Everything in the framework is oriented around request and response bodies that go through JSON, and there is no template layer, no view model and no HTML response helper anywhere in the documented API.

The design philosophy is stated as a goal rather than a feature list. Like the Go language itself, Tiger Tonic strives to keep features orthogonal, and it defers what it can to the Go standard library and a few other packages. That shows up in the shape of the API. There is no service object, no application lifecycle, no configuration framework bolted on top. You build an http.Handler, you wrap it in whatever middleware you want, and you serve it.

For who this is for: Go developers who like the language's composition style and want the conveniences Dropwizard popularised, request logging with redaction, metrics by status, basic auth, CORS headers, per-request context and a JSON configuration loader, all without leaving the net/http model. The documentation lives at pkg.go.dev for the package reference, and the README collects the articles and talks that explain the design, including a Go Advent 2013 piece and conference talks from GoSF 2014 and GopherCon 2014.

It is worth noting the age of that material. The framework has been shaped by a decade of conversation about Go web services, and the README still links to those early write-ups rather than a modernised manual.

## TrieServeMux turns path wildcards into query parameters

The routing entry point is tigertonic.TrieServeMux, and the README is blunt about why it exists: HTTP routing in the Go standard library is pretty anemic.

The mux accepts an HTTP method, a URL pattern, and either an http.Handler or an http.HandlerFunc. The distinctive part is the wildcard syntax. Components in the URL pattern wrapped in curly braces are wildcards, and the values of those wildcards do not cross slashes. The captured values are then added to the URL, where a handler reads them with the ordinary query accessor.

```go
func myHandler(*url.URL, http.Header, *MyRequest) (int, http.Header, *MyResponse, error)
```

That handler signature is the second half of the story, and it comes up in the Marshaled section below. For the router, the important consequence is that a pattern segment such as {id} becomes a readable query parameter rather than a named path value. The upside is one API for path and query parameters. The downside is that the path and the query string stop being distinguishable once the request is in the handler, so a route that takes both a path wildcard and a query parameter has to be read carefully.

There is a second method, HandleNamespace, which behaves like Handle but additionally strips the namespace from the URL. The documentation says this makes API versioning, multitenant services and relative links easier to manage, and describes it as roughly equivalent to http.ServeMux's behaviour. That is the piece to reach for when a service serves several versions of the same resource, because the version prefix does not have to be threaded through every handler.

## Marshaled turns a typed function into an http.Handler

Most of the framework's convenience comes from one wrapper. Wrap a function in tigertonic.Marshaled and it becomes an http.Handler. The function signature has to look like this, or Marshaled will panic.

```go
func myHandler(*url.URL, http.Header, *MyRequest) (int, http.Header, *MyResponse, error)
```

The four return values are status code, response headers, response body and error. Request bodies are unmarshaled into the MyRequest struct and response bodies are marshaled from MyResponse structs, so the JSON encoding of both directions is handled by the framework rather than by hand in every handler.

Errors are handled two ways. The tigertonic.HTTPEquivError interface is implemented by tigertonic.BadRequest and by the equivalent type for every other HTTP response status, so any error can be wrapped in the type matching the status you want.

```go
func myHandler(*url.URL, http.Header, *MyRequest) (int, http.Header, *MyResponse, error) {
    return 0, nil, nil, tigertonic.BadRequest{errors.New("Bad Request")}
}
```

Alternatively you can return a valid status as the first output parameter and an error as the last, and that status will be used in the error response. The zero status and nil headers in the example above are what the wrapper fills in.

Two extension points matter for anything that streams. If the return type implements io.Reader, the stream is written directly to the requestor, and a Content-Type header is required in the response headers, with the Accept header for those requests allowed to be anything. If the return type also implements io.Closer, the stream is closed automatically after it is flushed.

## Middleware as wrappers: logging, metrics, auth, CORS and context

Every cross-cutting feature in Tiger Tonic is an http.Handler wrapper, which is the concrete form of the orthogonality claim. Composition is left to the standard library.

Logging comes in three flavours. tigertonic.Logged writes request and response headers and bodies to standard output, and its second argument is an optional func(string) string called as requests and responses are logged, giving the caller a chance to redact sensitive information from log entries. tigertonic.JSONLogged produces the same output as JSON suitable for sending to ElasticSearch, Flume, Logstash and similar systems, prefixed with @json: . tigertonic.ApacheLogged writes the more traditional Apache combined log format. The redaction hook exists on the first two, which is the detail that makes them usable with credentials in headers.

Metrics come from go-metrics. tigertonic.Counted and tigertonic.Timed count or time requests. tigertonic.CountedByStatus and tigertonic.CountedByStatusXX count responses with a metrics.Uint64 for each HTTP status code or each family of status codes such as 1xx and 2xx.

The rest of the wrappers cover the rest of the checklist. tigertonic.HTTPBasicAuth takes a map[string]string of authorized usernames to passwords and requires a valid Authorization header. tigertonic.CORSHandler, built through CORSBuilder.Build, injects CORS headers, and currently only Origin related headers are supported. tigertonic.PostProcessed uses a TeeResponseWriter to record the response and call a func(*http.Request, *http.Response) after the response has been written, which is the hook for post-processing. tigertonic.WithContext wraps a handler and a zero value of any non-interface type to enable per-request context, read back with tigertonic.Context.

Two smaller pieces round out the set. tigertonic.First takes a variadic slice of handlers and calls ServeHTTP on each in succession until the first one that calls w.WriteHeader, and tigertonic.If expresses the most common form of that more naturally by calling the handler only when a func(*http.Request) (http.Header, error) returns no error. tigertonic.Configure reads and unmarshals a JSON configuration file into a configuration structure of your own design, which the documentation describes as mere convenience.

## HostServeMux puts several domain names on one listener

Most of the framework deals with one request at a time inside one process. HostServeMux deals with the network boundary. It serves multiple domain names from the same net.Listener, which means a single bound port can answer for several hosts.

That is a narrower feature than it first appears, and worth understanding precisely. This is not a reverse proxy and not a multi-tenant application layer. It dispatches on the Host header of the incoming request so that different hostnames reach different handlers, all sharing one listener and therefore one port.

For an organisation that runs several public hostnames on a single address, that removes the need for several processes or several port allocations. For anyone planning to use it as a tenant router for multitenant services, the README points instead at HandleNamespace, which strips a namespace from the URL rather than routing by hostname. Those two features solve similar problems at different layers, and the documentation keeps them separate.

The repository layout also hints at the internal structure of the framework, since almost every feature is a file of its own: trie_serve_mux.go, host_serve_mux.go, marshaler.go, logger.go, json_logger.go, metrics.go, middleware.go, http_basic_auth.go, cors.go, context.go, post_processor.go, tee.go, config.go, error.go, named_error.go, http_equiv_error.go, method_not_allowed.go, not_found.go, version.go, net.go, server.go and cache.go. Each one has a matching test file, which is a reasonable signal of how the project has been maintained over its life.

## Bootstrap, a single dependency and a Go 1.27 baseline

The usage section is short. Install dependencies with the bootstrap script.

```sh
sh bootstrap.sh
```

Then define your service, and the README points at the example directory in the repository as a possibly more convenient place to start. Requests that have bodies have types, and JSON is deserialized by adding tigertonic.Marshaled to your routes.

The module definition is short enough to read in full.

```go
module github.com/rcrowley/go-tigertonic

go 1.27.0

require go.withmatt.com/metrics v0.6.0
```

Two things stand out. The dependency list is one line: go.withmatt.com/metrics, which is the go-metrics package the README links for the counting and timing wrappers. There is no ORM, no config library, no logging framework dependency, and no HTTP router dependency, because the standard library supplies those roles. The exception is the metrics package, which the framework re-exports through its own wrappers.

The second thing is the go directive. The module declares go 1.27.0, so the framework now requires a recent Go toolchain even though its own code was written over a much longer period. Any team on an older Go version will need to upgrade before they can build it.

The example directory is the best starting point for a first use. It holds api.go, config.go, example.go, example.json and a test file, which together show a small service with its own configuration file and its handlers, and the mocking directory at the repository root suggests the same services are exercised against stubbed dependencies in tests.

## A custom licence and no tagged releases

The licence is the thing to look at first for an adopter. GitHub cannot classify it as one of the standard licences, and the LICENSE file at the root of the repository holds the actual terms. Treat that as a licence review item rather than an assumption: the usual shortcuts about permissiveness do not apply when a project uses a custom grant.

The maintenance picture is clearer. The repository is not archived and the last push was on 2026-09-03, so the tree is still receiving work. There are no GitHub releases at all, so there is no tag to pin a dependency to and no published release notes to read. Consumers normally depend on the module at a pseudo-version from the main branch, or on a commit hash.

The other visible constraint is the framework's own scope. The README's opening line is not a throwaway joke. There is no HTML support, and the documented API surface is entirely built around JSON request and response structs. There is also no database layer, no ORM integration, no migration tool and no dependency injection container in the documented features, so a service built with Tiger Tonic still needs the rest of its stack assembled by hand.

None of that is a defect so much as a boundary. The framework is a thin, composable layer over net/http for JSON APIs, and the value of adopting it is that the layer is small enough to read in an afternoon.

## Conclusion

go-tigertonic fits Go teams building JSON APIs that want Dropwizard style composition without adopting Dropwizard's Java ecosystem, especially where request and response logging, metrics and basic auth need to be wrapped around every route consistently. It does not fit projects that serve HTML, and it does not fit teams that need a permissively licensed dependency they can vendor without a licence review. Verify first that your route patterns work with the curly brace wildcard behaviour of TrieServeMux, then read the LICENSE file at the repository root, since GitHub cannot classify it and the terms decide whether you can ship it inside a closed source product.

## FAQ

### What is go-tigertonic used for?

Tiger Tonic is a Go framework for building JSON web services, inspired by Dropwizard. It provides routing, request and response marshaling, logging, metrics, basic auth, CORS headers and per-request context, and it says plainly that if HTML is your goal it will hurt a little.

### How do I set up a project that uses go-tigertonic?

Run the bootstrap script in the repository to install dependencies, then define your service. The example directory in the repository is described as a convenient place to start, and the module requires Go 1.27.0.

### How do URL parameters work in tigertonic.TrieServeMux?

Pattern components wrapped in curly braces are wildcards whose values do not cross slashes. Those values are added to the URL and are read in the handler with u.Query().Get("name").

### How does tigertonic.Marshaled handle request and response bodies?

The wrapped function takes a request struct pointer and a response struct pointer. Request bodies are unmarshaled into the request struct and response bodies are marshaled from the response struct, and the function must return a status, headers, response and error.

### Can I redact sensitive values from go-tigertonic logs?

Yes. tigertonic.Logged and tigertonic.JSONLogged take an optional func(string) string that is called as requests and responses are logged, which gives you the opportunity to redact sensitive information from log entries.

### What licence does go-tigertonic use?

It uses a custom licence that GitHub cannot classify, with the terms in the LICENSE file at the root of the repository. Read that file rather than assuming a standard open source licence applies.

## Sources

- [Issues](https://github.com/rcrowley/go-tigertonic/issues)
- [rcrowley/go-tigertonic on GitHub](https://github.com/rcrowley/go-tigertonic)
- [README](https://github.com/rcrowley/go-tigertonic/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/rcrowley-go-tigertonic
