# Tinka > A Go web framework: a small standard-library core and opt-in first-party packages that share one design language, built for agents to write and people to review. Every page below is also available as Markdown at its address plus `.md`. ## Start ### Getting started - [Introduction](https://tinka.tmuco.dev/introduction.md): What Tinka is, who it is for, what you can build with it today, and how stable it is during the alpha. - [Philosophy](https://tinka.tmuco.dev/philosophy.md): The nine principles every part of Tinka follows, and why each one exists. - [Pick your path](https://tinka.tmuco.dev/pick-your-path.md): Choose between the skeleton and core starter kits before you run tinka new, and find the guides for what you want to build first. - [Installation](https://tinka.tmuco.dev/installation.md): Install the tinka and act commands, and create your first App from a starter kit. - [Folder structure](https://tinka.tmuco.dev/folder-structure.md): Where everything goes in an App created from the skeleton starter kit, and how its packages depend on each other. - [Development environment setup](https://tinka.tmuco.dev/development.md): Run the App while you work with act serve, which rebuilds and restarts it on every change, shows build errors in the browser and reloads open pages. - [Configuration & Environment](https://tinka.tmuco.dev/configuration.md): Read typed, validated configuration from environment variables and .env files, and stop at boot with every problem listed. - [Deployment](https://tinka.tmuco.dev/deployment.md): Build the App into one static binary with act build, run it in production, and package it in a container. - [FAQs](https://tinka.tmuco.dev/faqs.md): Short answers to common questions about Tinka: stability, the Go version, net/http compatibility, dependencies, databases, the docs for agents and the license. ### Tutorial - [Tutorial overview](https://tinka.tmuco.dev/tutorial/overview.md): Build one Tinka App from an empty directory to a deployed binary: a JSON API for a blog's posts, with a Command and tests. - [Create the App](https://tinka.tmuco.dev/tutorial/create-the-app.md): Install tinka and act, create the blog App from the skeleton starter kit, and run it with act serve, which rebuilds it on every save. - [Routes and handlers](https://tinka.tmuco.dev/tutorial/routes-and-handlers.md): Give the blog App a store of posts, and add GET /posts and GET /posts/{id} with handlers that return values and a 404 for a missing post. - [Request bodies](https://tinka.tmuco.dev/tutorial/request-bodies.md): Add POST /posts to the blog App, decoding a JSON body with c.Body, validating every field at once, and answering 201 Created. - [Services and providers](https://tinka.tmuco.dev/tutorial/services-and-providers.md): Move the blog App's post store into the container with a service provider created by act make:provider, and resolve it where the routes register. - [Add a Command](https://tinka.tmuco.dev/tutorial/commands.md): Add posts:list to the blog App with act make:command, use the post store from the container, and return a result for people and agents. - [Test the App](https://tinka.tmuco.dev/tutorial/testing.md): Test the blog App's routes with tinkatest and its posts:list Command with clitest, swap the post store for a test, and run everything with act test. - [Deploy the App](https://tinka.tmuco.dev/tutorial/deploy.md): Build the blog App into one binary with act build, run it in production with its environment, probe its health checks, and run its Command from the binary. ### Resources - [Contributing](https://tinka.tmuco.dev/contributing.md): Work on Tinka itself: get the code, run the checks, build a package with its tests and docs, and commit on the version branch. - [Releases](https://tinka.tmuco.dev/releases.md): Every Tinka release by month, with what it holds and its tag. - [Roadmap](https://tinka.tmuco.dev/roadmap.md): The first-party packages planned after Tinka's foundation, and what Tinka leaves out on purpose. ## Guides ### Basics - [Routing](https://tinka.tmuco.dev/guides/basics/routing.md): Map URLs and HTTP methods to handlers that return values, with path parameters, groups, names and middleware. - [HTTP context](https://tinka.tmuco.dev/guides/basics/http-context.md): The *tinka.Ctx every handler and middleware receives: the request, the response, the request's ID and its dependencies. - [Middleware](https://tinka.tmuco.dev/guides/basics/middleware.md): Run code around handlers: for every request, for a group or for one route, with errors as values and net/http middleware through tinka.Std. - [Request](https://tinka.tmuco.dev/guides/basics/request.md): Read path parameters, the query string, headers, the client's IP behind proxies, and the content type the client prefers. - [Response](https://tinka.tmuco.dev/guides/basics/response.md): Return values that become responses: JSON, text, HTML, redirects, files and streams, with the status and headers you choose. - [Body parser](https://tinka.tmuco.dev/guides/basics/body-parser.md): Decode JSON, form and multipart bodies into typed values with c.Body, validate them, and read the raw body. - [File uploads](https://tinka.tmuco.dev/guides/basics/file-uploads.md): Accept files in multipart forms as typed fields, validate their size and type, and save them safely. - [URL builder](https://tinka.tmuco.dev/guides/basics/url-builder.md): Name routes and build their URLs from the name, with parameters, wildcards and query strings escaped for you. - [Exception handling](https://tinka.tmuco.dev/guides/basics/exception-handling.md): Return expected failures as tinka.Error, map Capability errors with ErrorMap, choose what is logged, and render errors your own way. ### Security - [Hashing](https://tinka.tmuco.dev/guides/security/hashing.md): Hash passwords with Argon2id, verify them, rehash them when settings change, and move users from scrypt or bcrypt as they sign in. - [Encryption](https://tinka.tmuco.dev/guides/security/encryption.md): Encrypt and sign values with the App's key, bind them to a purpose, let them expire, rotate keys, and look up encrypted values with blind indexes. ### Core Concepts - [Application lifecycle](https://tinka.tmuco.dev/guides/concepts/application-lifecycle.md): How an App boots, starts, serves and shuts down gracefully, and where your setup and cleanup code goes. - [Dependency injection](https://tinka.tmuco.dev/guides/concepts/dependency-injection.md): Build and share an App's services with the di container: typed bindings, lifetimes, request scope, validation at boot and swaps in tests. - [Service providers](https://tinka.tmuco.dev/guides/concepts/service-providers.md): Package a service with its setup and cleanup: register bindings, configure, start, and release resources with the App's lifecycle. - [Scaffolding](https://tinka.tmuco.dev/guides/concepts/scaffolding.md): Create handlers, providers, Commands, middleware and tests with act make, register them in the App's code, and change the stubs they're written from. ### Digging Deeper - [Emitter](https://tinka.tmuco.dev/guides/digging-deeper/emitter.md): Announce that something happened with typed events, react with listeners, run them in the background, and fake them in tests. - [Health checks](https://tinka.tmuco.dev/guides/digging-deeper/health-checks.md): Report liveness and readiness to load balancers and orchestrators, check dependencies with deadlines and caching, and drain traffic before shutting down. - [Logger](https://tinka.tmuco.dev/guides/digging-deeper/logger.md): Write structured logs with log/slog, tagged with each request's ID, readable in a terminal and JSON in production. ### Command line - [Command line](https://tinka.tmuco.dev/guides/command-line/introduction.md): Run your App's Commands with act, list them, read their help, and run them from the App's own binary in production. - [Creating Commands](https://tinka.tmuco.dev/guides/command-line/commands.md): Write your own Commands as Go values, return results for people and agents, use the App's services, and guard Commands that change data. - [Arguments](https://tinka.tmuco.dev/guides/command-line/arguments.md): Declare a Command's positional arguments, make them required or optional, accept many values, parse them as types, and ask for missing ones. - [Flags](https://tinka.tmuco.dev/guides/command-line/flags.md): Declare a Command's flags with the standard library's flag types, add short names and environment variables, and tell which flags were passed. - [Prompts](https://tinka.tmuco.dev/guides/command-line/prompts.md): Ask people for text, passwords, choices and confirmations, with each answer also taken from a flag, so scripts and agents never wait for input. - [Terminal output](https://tinka.tmuco.dev/guides/command-line/terminal-output.md): Show what a Command is doing with progress, status lines, tasks, details and lists, styled in terminals and plain in logs. ### Testing - [Testing](https://tinka.tmuco.dev/guides/testing/introduction.md): Decide what to test where, write the App's tests with Go's testing package and tinkatest, and run them with act test. - [HTTP tests](https://tinka.tmuco.dev/guides/testing/http-tests.md): Send requests to the App in a test with tinkatest, assert on statuses, JSON, cookies and redirects, and test what depends on time. - [Test doubles](https://tinka.tmuco.dev/guides/testing/test-doubles.md): Replace the App's services in a test with fakes of your own, fake its events, and keep its tests fast and independent. - [Testing Commands](https://tinka.tmuco.dev/guides/testing/commands.md): Run Commands in tests with clitest, check their results and output, answer their prompts by flag name, and keep their signatures stable. ## Reference - [App](https://tinka.tmuco.dev/reference/app.md): The tinka.App type, its configuration, its methods, and everything it does for every request by default. - [Commands](https://tinka.tmuco.dev/reference/commands.md): Every Command that ships with Tinka, with its flags and output: tinka new, act serve, act build, act test, the act make generators, act about, act trust and act generate:key. - [Agent mode](https://tinka.tmuco.dev/reference/agent-mode.md): How Tinka Commands answer when an AI agent runs them, with one JSON result per run. - [Environment variables](https://tinka.tmuco.dev/reference/environment-variables.md): Every environment variable Tinka reads or sets, grouped by purpose, with what reads it, its values and its default. - [Error codes](https://tinka.tmuco.dev/reference/error-codes.md): Every error code Tinka's Commands and HTTP responses return, with when it happens, what returns it and how to fix it. --- Source: https://tinka.tmuco.dev/introduction.md # Introduction > What Tinka is, who it is for, what you can build with it today, and how stable it is during the alpha. Tinka is a web framework for Go: a small Core plus opt-in first-party packages that share one design language. It gives an App routing, dependency injection, configuration, Commands and tests in one consistent shape, built on the standard library instead of around it. ## Who is Tinka for? Tinka is for Go teams who want conventions without leaving `net/http`. Routes register on the standard library’s `http.ServeMux`, every Handler receives an [HTTP context](guides/basics/http-context.md) that is a `context.Context`, and middleware written for `net/http` joins the chain through `tinka.Std`. You keep the Go you know and stop deciding, project after project, how an App is laid out, configured and tested. It is also for agents that write code, and for the people who review what they write. When there is one way to define a route, read configuration or write a Command, an agent gets the change right on the first attempt, and the reviewer reads a diff shaped like every other change in the App. ## Why Tinka Every part of Tinka follows nine principles. Four of them shape the code you write every day: - **One obvious way**: each task has one way to do it, so conventions replace the decisions every project would otherwise make again. - **Explicit wiring**: there is no reflection magic and no hidden global state. You can follow any request or Command from `main()` to the code that handles it. - **Standard library first**: Tinka types work with `net/http`, `context`, `log/slog` and `errors` instead of replacing them, and nothing runs before `go build` or wraps `go test`. - **Docs are product**: nothing ships without its docs page, written for the people and the agents who read it. The [philosophy](philosophy.md) explains all nine principles and the problem each one solves. ## What can you build Tinka covers the server side of an App that answers over HTTP and from the terminal: - **JSON APIs**: a Handler returns a value and Tinka sends it as JSON. The [body parser](guides/basics/body-parser.md) decodes JSON and form bodies into typed values, [file uploads](guides/basics/file-uploads.md) arrive as typed fields, and [exception handling](guides/basics/exception-handling.md) turns errors into problem details responses. - **Services with their own Commands**: an App’s binary serves requests when run without arguments and runs one of its [Commands](guides/command-line/commands.md) when given one, so tasks such as imports ship with the App and run in production from the same binary. - **Background work**: the [emitter](guides/digging-deeper/emitter.md) runs listeners in the background with `EmitAsync`, and the App waits for them when it shuts down. - **Services behind a load balancer**: [health checks](guides/digging-deeper/health-checks.md) report liveness and readiness and drain traffic before shutdown, and `act build` turns the App into one static binary for [deployment](deployment.md). Tinka has no data layer, view layer, sessions, authentication or mail yet. Until it does, open a database with the driver of your choice and register its connection pool in a [service provider](guides/concepts/service-providers.md), and render pages with the standard library’s `html/template`, returned as [`tinka.HTML`](guides/basics/response.md). ## Built for agents Every Tinka Command answers people with readable text and answers agents with a single JSON object, detected automatically: see [Agent mode](reference/agent-mode.md). Every Prompt is answered by a flag too, so an agent never waits for input. Every docs page has a Markdown twin at the same address plus `.md`, and `llms.txt` at the root of the docs site lists every page with the address of its twin. ## Status and stability Tinka is in early alpha. Today it ships: - The Tinka CLI, which creates Apps from [starter kits](installation.md), and `act`, which runs an App’s Commands, [serves it while you work](development.md) and builds it for [deployment](deployment.md). Every Command answers agents in [Agent mode](reference/agent-mode.md). - The HTTP layer of Core: [routing](guides/basics/routing.md), [middleware](guides/basics/middleware.md), the [body parser](guides/basics/body-parser.md), [file uploads](guides/basics/file-uploads.md) and [exception handling](guides/basics/exception-handling.md). - The [App lifecycle](guides/concepts/application-lifecycle.md), [dependency injection](guides/concepts/dependency-injection.md), [service providers](guides/concepts/service-providers.md) and [configuration from environment variables](configuration.md). - [Hashing](guides/security/hashing.md), [encryption](guides/security/encryption.md), the [logger](guides/digging-deeper/logger.md), the [emitter](guides/digging-deeper/emitter.md) and [health checks](guides/digging-deeper/health-checks.md). - [Commands](guides/command-line/introduction.md) of your own, with arguments, flags and prompts. - [Tests](guides/testing/introduction.md) that drive the App through HTTP and the command line, with no network. :::note APIs will change before `v1.0.0`. Pin the version you build against in `go.mod`. ::: ## Next steps - [Install the Tinka CLI](installation.md) and create your first App. - Follow the [Tutorial](tutorial/overview.md) to build an App step by step. - Not sure which starter kit to start from? [Pick your path](pick-your-path.md). --- Source: https://tinka.tmuco.dev/philosophy.md # Philosophy > The nine principles every part of Tinka follows, and why each one exists. Every decision in Tinka traces back to nine principles. They are the reason the framework looks the way it does, and they settle disagreements before they start. When a proposed feature breaks one, the feature changes, not the principle. ## Not a port Tinka borrows familiar ideas from AdonisJS, Leaf and Laravel, but it does not copy their APIs. A Go framework should feel like Go: explicit, typed and close to the standard library. Some ideas that make sense in TypeScript or PHP have no place here, and some problems get solved in a different way. ## One obvious way Each task has one obvious way to do it. Flexibility sounds generous, but every option is a decision someone has to make and a variation someone has to read later. Strong conventions let a person review a change quickly and let an agent write it correctly the first time. ## Terse at the call site, explicit in the wiring Using Tinka takes little code, and every piece of that code is traceable. There is no reflection magic and no hidden global state: you can follow any request or Command from `main()` to the code that handles it. Short code that hides its wiring costs more to debug than it saves to write. ## Standard library first Tinka types work with `net/http`, `context`, `log/slog` and `errors` instead of replacing them. Your existing Go knowledge applies, and standard library packages, tools and middleware keep working. ## Full-stack, opt-in Common web needs are solved by first-party packages: routing, configuration, logging and health checks today, and validation, sessions and a data layer next, as the [Roadmap](roadmap.md) lists. You import only the ones you use, so an App never carries code or dependencies it does not need. ## One design language Every package shares the same naming, configuration, errors and testing patterns. Once you have learned one package, the next one reads the same way. ## Docs are product Nothing ships without its docs page. A feature that is hard to explain is usually hard to use, and writing the docs is how Tinka finds out early. ## Every dependency is a liability Each dependency is chosen deliberately and the reason is recorded. Dependencies bring updates, security advisories and behavior you do not control, so Tinka prefers a short piece of its own code to a large library it uses a fraction of. ## Zero overhead Tinka never slows the Go toolchain. No code generation step runs before `go build`, and nothing wraps `go test` or `go run`. Whatever Tinka adds on top is measured before it ships: the Tinka CLI starts in about 14 ms, faster than `go version`. At runtime, an abstraction costs no more than the `net/http` code it replaces. --- Source: https://tinka.tmuco.dev/pick-your-path.md # Pick your path > Choose between the skeleton and core starter kits before you run tinka new, and find the guides for what you want to build first. `tinka new` creates every App from a **starter kit**: a ready-to-run App that it copies into a new directory, under your module path. Tinka has two official starter kits, `skeleton` and `core`, embedded in the Tinka CLI, so they always match the version of Tinka you installed. They differ in how much structure you start with, and this page helps you choose before you run `tinka new`. ## The two starter kits `tinka new` asks which kit to use, and shows what each one is beside its name: ```text title="Output" ? Which starter kit would you like to use? (↑/↓ and Enter) ❯ skeleton An App with Tinka's conventions, ready to grow core The router and nothing else: structure it your way ``` Both kits give you a `go.mod` that pins `act` as a tool, a `README.md` listing `act serve`, `act test` and `act build`, a `.gitignore`, a home route and a test of it. Everything else differs: | | `skeleton` | `core` | |---|---|---| | Layout | A package per kind of code: `app`, `config`, `start` and `tests` | One `main.go` in the App’s root package | | Configuration | A typed `config` package that reads environment variables and `.env` files, and reports every missing or invalid one at boot | `tinka.HTTPConfig{}` in code, with Tinka’s defaults | | `APP_KEY` | Generated into `.env` by `tinka new`, with a test key in `.env.test` | None | | Wired up for you | The logger, the encrypter, an exception handler, a 30 second request timeout and health check routes | The router | | `GET /` returns | `{"hello":"world"}` as JSON | `Hello from Tinka` as text | | Tests | `tests.App` builds the App for tests, and `tests/functional` holds HTTP tests written with `tinkatest` | `main_test.go`, written with `net/http/httptest` | | Generators write to | `app/handlers`, `app/providers` and the other packages, registered in `start` | The root package, registered after `tinka.New` | ## The skeleton starter kit The skeleton is the default. It gives every App the same shape, so you, your team, agents and the [generators](guides/concepts/scaffolding.md) such as `act make:handler` all know where code goes: ```text blog/ ├── app/ │ ├── exceptions/ │ │ └── handler.go │ └── handlers/ │ └── home.go ├── config/ │ ├── app.go │ ├── config.go │ ├── http.go │ └── logger.go ├── start/ │ ├── app.go │ ├── kernel.go │ └── routes.go ├── tests/ │ ├── functional/ │ │ └── home_test.go │ └── tests.go ├── main.go ├── go.mod ├── README.md ├── .env ├── .env.example ├── .env.test └── .gitignore ``` Pick the skeleton when you are starting a new App, when several people or agents will work on it, or when you want the docs and generators to match your code exactly. [Folder structure](folder-structure.md) explains each directory and which package may import which. ## The core starter kit The core kit keeps the whole App in `main.go`. Its `newApp` function creates the App, registers one route and returns it, and `main` runs it: ```text blog/ ├── main.go ├── main_test.go ├── go.mod ├── README.md └── .gitignore ``` Pick the core kit when you already have a structure in mind, when you are learning how the pieces fit before taking on conventions, or when you are adding Tinka to an existing Go module. Every Tinka package works on its own, so you add [configuration](configuration.md), [service providers](guides/concepts/service-providers.md) and the rest as you need them. The generators work here too: they write into the root package and register what they create in the function that calls `tinka.New`. ## How to choose Answer one question: do you want Tinka to decide where your code goes? - **Yes, or you are not sure**: pick `skeleton`. It is what `tinka new` creates when you press Enter. - **No, you have your own layout**: pick `core`, and structure the App as it grows. Choose up front with `--kit` to skip the question, such as in a script or when an agent runs `tinka new`: ```sh tinka new blog --kit core ``` See the [`tinka new` reference](reference/commands.md#tinka-new) for every flag. ## Where to go next Once [Installation](installation.md) has created your App, follow the path for what you want to build first: | Goal | Start with | |---|---| | A guided build of a whole App | The [Tutorial](tutorial/overview.md) | | Serve a JSON API | [Routing](guides/basics/routing.md), then the [HTTP context](guides/basics/http-context.md), [Response](guides/basics/response.md) and the [body parser](guides/basics/body-parser.md) | | Report errors to clients | [Exception handling](guides/basics/exception-handling.md) | | Share services such as a database pool | [Dependency injection](guides/concepts/dependency-injection.md) and [service providers](guides/concepts/service-providers.md) | | Write Commands for the terminal | The [command line introduction](guides/command-line/introduction.md), then [Creating Commands](guides/command-line/commands.md) | | Test the App | The [testing introduction](guides/testing/introduction.md), then [HTTP tests](guides/testing/http-tests.md) and [console tests](guides/testing/commands.md) | | Run it in production | [Deployment](deployment.md) | See also: [Installation](installation.md), [Folder structure](folder-structure.md), [Scaffolding](guides/concepts/scaffolding.md) --- Source: https://tinka.tmuco.dev/installation.md # Installation > Install the tinka and act commands, and create your first App from a starter kit. Tinka has two Commands you install with Go’s own tooling: the Tinka CLI, `tinka`, creates Apps and reads the docs, and `act` runs Commands inside an App. It needs Go 1.27 or later; check with `go version`. :::warning During the alpha, the Tinka repository is private, so `go install` fails with a 404 or an authentication error unless your GitHub account has access. Tell Go to fetch Tinka directly from GitHub instead of the public module proxy, and make sure `git` can authenticate to GitHub: ```sh go env -w GOPRIVATE=github.com/mucolabs/* ``` ::: ## Install Install the latest release: ```sh go install github.com/mucolabs/tinka/cmd/tinka@latest ``` Go places the binary in `$(go env GOPATH)/bin`. Make sure that directory is on your `PATH`. ## Check the installation Run `tinka version`: ```sh tinka version ``` ```text title="Output" tinka v1.0.0-alpha.1 (go1.27.2, linux/amd64) ``` When an agent runs the same Command, it gets one JSON line instead. Add `--json` to see it yourself: ```sh tinka version --json ``` ```text title="Output" {"command":"version","result":"ok","version":"v1.0.0-alpha.1","go":"go1.27.2","platform":"linux/amd64","duration_ms":0} ``` See [Agent mode output](reference/agent-mode.md) for the full format. ## Install act `act` runs Commands inside an App, such as `act generate:key`. `tinka` creates Apps and reads the docs; `act` works in the App's directory. Install it the same way: ```sh go install github.com/mucolabs/tinka/cmd/act@latest ``` Check it with `act --version`, and list its Commands with `act --help`: ```sh act --help ``` ```text title="Output" Run an App's Commands Usage: act [flags] Commands: serve Run the App, and rebuild and restart it when its files change build Build the App for production test Run the App's tests generate:key Generate a random APP_KEY and write it to .env trust Trust the App in this directory, so act builds and runs its code completion Print a shell completion script Flags: -h, --help Show help --json Print a single JSON result instead of text --version Print the version ``` ## Create an App Run `tinka new` with the directory to create. It asks which starter kit to start from, and whether to initialize a git repository: ```sh tinka new blog ``` ```text title="Output" ? Which starter kit would you like to use? (↑/↓ and Enter) ❯ skeleton An App with Tinka's conventions, ready to grow core The router and nothing else: structure it your way ? Initialize a git repository? (Y/n) Copying the skeleton kit ............................................ 3ms DONE Installing dependencies ........................................... 1.2s DONE Initializing a git repository ...................................... 10ms DONE Created blog with the skeleton kit cd blog act serve ``` `tinka new` copies the kit, requires the version of Tinka it was installed at and downloads the dependencies. For the skeleton kit, it also generates the App's `APP_KEY` into `.env`. Run `act serve` in the new directory, and open `http://localhost:3000`. Choose the kit up front with `--kit` to skip the question, as in a script: ```sh tinka new blog --kit core ``` ### Official starter kits Tinka offers two official starter kits. Each sets up an App in a different way, depending on how much structure you want from the start. - **Skeleton Starter Kit.** An App with Tinka's conventions in place: configuration read from the environment, routes and middleware in one place, and tests ready to run. Ideal when you want to start building right away, with [generators](guides/concepts/scaffolding.md) such as `act make:handler` following one structure. See [Folder structure](folder-structure.md). - **Core Starter Kit.** A single `main.go` with the router. Ideal when you already have a structure in mind, or are adding Tinka to an existing Go module: every Tinka package works on its own. More kits are on the way: API, API monorepo, Hypermedia, Inertia, and a domain-driven kit. ### The module path The **module path** names a Go project, on the first line of its `go.mod`, and prefixes the imports of its own packages: in an App whose module path is `blog`, the `start` directory is imported as `blog/start`. When a project is published, the module path is also where `go get` downloads it from, which is why many are repository addresses. `tinka new` names the module after the directory. An App nobody imports needs nothing more, but you can pass a full path: ```sh tinka new blog --module github.com/acme/blog ``` See also: [Folder structure](folder-structure.md), [Development environment setup](development.md), [Commands reference](reference/commands.md#tinka-new) --- Source: https://tinka.tmuco.dev/folder-structure.md # Folder structure > Where everything goes in an App created from the skeleton starter kit, and how its packages depend on each other. This guide covers the folder structure of an App created from the skeleton starter kit. You will learn: - What each top-level directory holds - How `main.go` and `start` assemble the App - Where your own code goes as the App grows - Which package may import which ## Overview The skeleton gives every App the same shape, so you, your team, the docs and [generators](guides/concepts/scaffolding.md) such as `act make:handler` all know where things go. Every directory is a Go package, imported by the App's module path, such as `blog/start`. ```text blog/ ├── app/ │ ├── exceptions/ │ └── handlers/ ├── config/ ├── start/ ├── tests/ │ └── functional/ ├── main.go ├── go.mod ├── .env.example └── .env.test ``` The core starter kit has none of this: a single `main.go`, to structure as you choose. ## main.go `main.go` is the App's entry point. It reads the configuration with `config.Load`, builds the App with `start.App`, and hands it the command line: ```go title="main.go" package main import ( "fmt" "os" "blog/config" "blog/start" ) func main() { cfg, err := config.Load() if err != nil { fmt.Fprintln(os.Stderr, err) // every missing or invalid variable, at once os.Exit(1) } app, err := start.App(cfg) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } os.Exit(app.Main(os.Args[1:])) } ``` One binary does everything: with no arguments it serves requests, and with arguments it runs one of the App's [Commands](guides/command-line/introduction.md), such as `./blog about`. `act` runs the same binary for you during development. ## start `start` assembles the App, in three files: ::::options :::option{name="start/app.go" type="file"} `App` creates the App and its services from the configuration, and adds the middleware and routes. `main` runs the App it returns, and tests build it the same way, with a configuration of their own. ::: :::option{name="start/kernel.go" type="file"} `Kernel` adds the middleware every request passes through, in order. ::: :::option{name="start/routes.go" type="file"} `Routes` registers the App's routes. It runs when the App starts, after the providers registered their services, so it can resolve them, and an error it returns stops the App before it serves. ::: :::: ## app `app` holds your code, in a package for each kind: `app/handlers` for [handlers](guides/basics/routing.md), `app/middleware` for [middleware](guides/basics/middleware.md), `app/providers` for [service providers](guides/concepts/service-providers.md), `app/commands` for [Commands](guides/command-line/commands.md), and `app/exceptions`, whose `Handler` decides how errors reach clients. A new App has `handlers` and `exceptions`; the others appear as you add to them. ## config `config` reads the App's configuration from the environment, once, at boot, into typed structs: `config/app.go` for the App's URL and key, `config/http.go` for the HTTP server, `config/logger.go` for logging. `config.Load` reports every missing or invalid variable together, so a misconfigured App stops at boot. See [Configuration & Environment](configuration.md). `.env.example` lists the variables the App reads, without their values. `.env`, which `tinka new` creates with a fresh `APP_KEY`, holds your machine's values and stays out of git. `.env.test` holds the values tests use, and is committed. ## tests `tests` holds what tests share. `tests.App` builds the App as `main` does, in the `test` Mode, with a fast password hasher and its logs in the test's output, without starting it: [`tinkatest`](guides/testing/http-tests.md) starts it. HTTP tests that exercise the whole App go in `tests/functional`: ```go title="tests/functional/home_test.go" package functional_test import ( "net/http" "testing" "github.com/mucolabs/tinka/tinkatest" "blog/tests" ) func TestHome(t *testing.T) { t.Parallel() c := tinkatest.New(t, tests.App(t)) c.Get("/").Send(t). AssertStatus(http.StatusOK). AssertJSON(map[string]string{"hello": "world"}) } ``` Tests of a single package go next to it, as Go tests do, such as `app/handlers/home_test.go`. Run them all with [`act test`](guides/testing/introduction.md). ## Which package imports which Go refuses two packages that import each other, so the skeleton's packages depend in one direction: - `main` imports `start`. - `start` imports `config` and `app/...`. - `app/...` packages import Tinka, and each other where one builds on another, but never `start` or `main`. A handler that needs a service gets it from the App's [container](guides/concepts/dependency-injection.md), with `c.Make`, rather than importing the package that creates it. That keeps every dependency pointing toward `start`, where the App is assembled. See also: [Installation](installation.md), [Configuration & Environment](configuration.md), [Application lifecycle](guides/concepts/application-lifecycle.md) --- Source: https://tinka.tmuco.dev/development.md # Development environment setup > Run the App while you work with act serve, which rebuilds and restarts it on every change, shows build errors in the browser and reloads open pages. This guide covers the development environment for Tinka Apps. You will learn how to: - Run the App with `act serve`, which rebuilds and restarts it when you change a file - Read build errors in the terminal and in the browser - Reload open pages after each change - Watch files on network drives and in containers - Set up your code editor ## Overview While you work, the App should follow your edits without you stopping and starting it. `act serve` builds the App, runs it, and watches its files. When you save a change, it builds the new App while the old one keeps serving, then restarts it in a few milliseconds. Requests that arrive during the restart wait instead of failing, and pages open in your browser reload. `act serve` runs the App's own binary, the one that runs in production, built with the same `go build`. Nothing is generated before the build, and nothing about the App changes in development: shutdown hooks run on each restart exactly as they do when your platform stops the App. ## Run the dev server Run `act serve` from the App's directory: ```sh act serve ``` ```text title="Output" INFO Watching for changes (inotify, 12 directories) INFO Built in 2.0s, serving http://localhost:3000 ``` Open the address it prints. Building and running the App runs its code, so the first time, `act` asks whether you trust the App's directory. See [act trust](reference/commands.md#act-trust). `act serve` serves on `localhost:3000`. Pass `--addr` for another address, such as `--addr :8080` to accept connections from other machines on your network: ```sh act serve --addr localhost:8080 ``` The App's own `Addr` doesn't apply under `act serve`: the dev server decides where the App listens, and passes it the listener. `act serve` answers requests for `localhost`, its subdomains such as `blog.localhost`, and IP addresses. A request for any other name gets a `403`, because a web page you visit could otherwise point a name of its own at your machine and read your App through your browser. To open the App under a name of your own, such as one in `/etc/hosts`, allow it: ```sh act serve --allow-host blog.test ``` ## What happens on a change `act serve` reacts to the files a build reads: Go files, `go.mod`, `go.sum` and files your code embeds. A save that changes none of them, such as an editor's swap file, starts no build. For a change that does: ::::steps :::step{title="Build"} The new App is built while the old one keeps serving the previous code. A change made during a build cancels it and starts again from the newest files. ::: :::step{title="Stop the old App"} The old App receives `SIGTERM` and shuts down gracefully: requests in flight finish and shutdown hooks run. It is killed if it hasn't stopped after 15 seconds. ::: :::step{title="Start the new App"} The new App starts on the same listener. Requests that arrived in between wait in the queue, so none fails. ::: :::: ```text title="Output" INFO Built in 1.3s, restarted in 37ms ``` When the App stops on its own, such as when it fails to connect to its database at startup, `act serve` shows its output and starts it again on your next change. ## Read build errors When a build fails, the old App keeps running and `act serve` prints the compiler's errors: ```text title="Output" ERROR The build failed after 412ms; the App keeps running its previous code # example.com/blog ./posts.go:14:9: undefined: render ``` Pages you open in the browser show the same errors instead of the App, so you see them where you're looking. Requests that don't come from a browser page, such as `fetch` calls and API clients, still reach the old App. Fixing the error swaps the new App in. ## Reload open pages After each restart, and when a build fails, pages open in the browser reload on their own. `act serve` adds a small script, `/__tinka/live.js`, before the closing `` of every full HTML page the App returns. Fragments, such as htmx's partial responses, and anything that isn't HTML are left as they are. The script is a file of its own, so a `Content-Security-Policy` of `script-src 'self'` allows it. ## Watch network drives and containers `act serve` learns about changes from the operating system on Linux and Windows. Some file systems never report changes, such as network drives, folders shared into a virtual machine or a container, and paths under `/mnt` in WSL. Pass `--poll` to check for changes on a timer instead: ```sh act serve --poll ``` On macOS, `act serve` always checks on a timer. A check takes a few milliseconds for an App of a few hundred files. ## Serve without the proxy `act serve` listens on `--addr` and forwards requests to the App, which is what lets it hold requests during a restart, show build errors and reload pages. Pass `--no-proxy` to give the App the address directly, with none of these: ```sh act serve --no-proxy ``` On Linux and macOS the port still stays open across restarts. On Windows, connections made during a restart fail. ## Set up your editor Tinka is plain Go, so every editor with Go support works. Install [gopls](https://go.dev/gopls), the Go team's language server, through your editor's Go extension, and turn on formatting on save, so every file stays `gofmt`ed. See also: [Commands reference](reference/commands.md#act-serve), [Application lifecycle](guides/concepts/application-lifecycle.md), [Configuration & Environment](configuration.md) --- Source: https://tinka.tmuco.dev/configuration.md # Configuration & Environment > Read typed, validated configuration from environment variables and .env files, and stop at boot with every problem listed. This guide covers configuration in Tinka. You will learn about: - Environment variables and the `.env` file - The `APP_KEY` and how to generate it - Reading variables into your App at boot - Variable interpolation within `.env` files - Reading values from files, such as Docker secrets - Environment-specific `.env` files for development, testing and production - Validating environment variables with typed getters - Writing `.env` files from a Command ## Overview An App reads the settings that change between machines, such as its port, its URL and its API keys, from environment variables, so the same binary runs on your laptop, in tests and in production. The `env` package (`github.com/mucolabs/tinka/env`) reads the real environment and your `.env` files, converts each variable to the Go type your App needs, and collects every missing or invalid one. A misconfigured App stops at boot with the whole list of problems, instead of failing later on the first one it meets. ## Environment variables Environment variables hold secrets and settings that differ between environments. During development you keep them in a `.env` file, one `KEY=value` per line. In production you set them in your hosting platform's settings or your container definition. ```dotenv title=".env" HOST=localhost PORT=3333 APP_URL=http://localhost:3333 APP_KEY=dev_only_key_1234567890 MAIL_FROM=hello@example.com STRIPE_KEY=sk_test_1234567890 ``` The format is the common dotenv format, so a `.env` file shared with other tools means the same thing in all of them: - lines starting with `#` are comments, and so is anything after `#` in an unquoted value - spaces around the key and the value are ignored, and an `export ` prefix is allowed - values in `'…'`, `"…"` or `` `…` `` keep their spaces and `#` characters and can span several lines, which suits certificates; inside `"…"`, `\n` becomes a newline - a key starts with a letter or `_`, followed by letters, digits, `_`, `.` or `-` Tinka never guesses what a broken line means. It reports the file and line of each one instead: a line without `=`, an invalid key, an unclosed quote, and text after a closing quote. :::warning A `.env` file holds real credentials. Committing it publishes them to everyone with access to the repository. Ignore `.env` and every local override, as the skeleton starter kit's `.gitignore` does. Commit `.env.example`, and the files that hold no secrets of yours, such as `.env.test` with its test-only key: ```text title=".gitignore" .env .env.local .env.*.local ``` ::: ### The APP_KEY `APP_KEY` is your App's secret key. The [encryption](guides/security/encryption.md) package uses it to encrypt data and to sign cookies and URLs. Generate it with `act generate:key`, which writes a random 256-bit key to `.env` and adds an empty `APP_KEY=` to `.env.example`, so your team knows to set one without seeing yours: ```sh act generate:key ``` ```text title="Output" Wrote APP_KEY to .env and .env.example ``` Anyone who has the key can read what it encrypted and forge what it signs, so keep it out of the repository and use a different key in each environment. Production reads no `.env` file, so when `APP_ENV` is `production` the Command prints the key instead of writing it; set it in your platform's environment. `act generate:key --show` prints a key in any Mode. If a key leaks, generate a new one. To replace a key that didn't leak, [rotate it](guides/security/encryption.md#rotate-the-apps-key), so values made with the old key keep working meanwhile. ### Using environment variables in your App Read environment variables once, at boot, into a configuration struct, and pass that struct to the code that needs it. Code that calls `os.Getenv` deep inside a handler hides what the App depends on and fails long after startup. `env.Load` returns an Env, each getter reads one variable as a Go type, and `Err` reports every problem at once: ```go title="config.go" package main import ( "net/url" "time" "github.com/mucolabs/tinka/env" ) type Config struct { Port int AppURL *url.URL AppKey env.Secret ReadTimeout time.Duration } func loadConfig() (Config, error) { e := env.Load() cfg := Config{ Port: e.Int("PORT", 3333), AppURL: e.URL("APP_URL"), AppKey: e.Secret("APP_KEY"), ReadTimeout: e.Duration("READ_TIMEOUT", 10*time.Second), } return cfg, e.Err() } ``` `main` stops the App when the configuration is wrong: ```go title="main.go" package main import ( "fmt" "os" ) func main() { cfg, err := loadConfig() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Println("listening on port", cfg.Port) } ``` Run it with an invalid port and no `APP_URL` or `APP_KEY`, and the error lists all three problems: ```sh PORT=80a go run . ``` ```text title="Output" env: 3 problems: PORT must be a whole number, got "80a" (environment) APP_URL is missing APP_KEY is missing ``` `env.Load` never changes the process environment: the values it reads stay inside the Env. ### Variable interpolation A value in a `.env` file can use another variable with `$NAME`, which saves repeating a host or a port: ```dotenv title=".env" HOST=localhost PORT=3333 APP_URL=http://$HOST:$PORT ``` `APP_URL` reads as `http://localhost:3333`. Wrap the name in braces when it contains a dash or touches other text: ```dotenv title=".env" BUCKET=blog REGION=eu-west-1 ASSETS_URL=https://${BUCKET}.s3.${REGION}.amazonaws.com ``` A reference can point to a variable in another `.env` file or in the real environment, which wins when both define it. A variable that isn't set anywhere expands to an empty string, and two variables that refer to each other are reported. Write `\$` for a literal dollar sign, or single-quote the value, which is never expanded, as in a shell: ```dotenv title=".env" PRICE=\$99 PASSWORD='pa$$word' ``` ### Reading values from files Docker and Kubernetes mount secrets as files, so the secret never sits in the environment. When `KEY` is missing and `KEY_FILE` is set, the getter reads the value from the file `KEY_FILE` names: ```dotenv title=".env" DB_PASSWORD_FILE=/run/secrets/db_password GCS_KEY_FILE=gcs_key.json ``` `e.Secret("DB_PASSWORD")` returns the file's contents, with one trailing newline dropped. A relative path is resolved against the directory the `.env` files are loaded from. A file that can't be read is reported at boot. `KEY` itself always wins over `KEY_FILE`. An identifier resolves values that start with a prefix you choose, such as `base64:`. Pass `env.Identifier` to `env.Load` with the function that turns the rest of the value into the real one: ```go e := env.Load(env.Identifier("base64", func(v string) (string, error) { b, err := base64.StdEncoding.DecodeString(v) return string(b), err })) token := e.Secret("API_TOKEN") // API_TOKEN=base64:czNjcmV0 reads as s3cret ``` An identifier applies to values from the real environment and from the files alike. A value it can't resolve is reported at boot. Write `base64\:` to keep a value that starts with `base64:` as it is. Identifiers belong to the Env you pass them to, so two Apps in one test binary never share them. ### Environment-specific .env files The **Mode** says how an App runs: `development`, `test` or `production`. It comes from `APP_ENV`, defaults to `development`, and decides which `.env` files `env.Load` reads: | File | Purpose | Git status | |---|---|---| | `.env` | Base variables for every Mode | Ignored | | `.env.local` | Local overrides, loaded in every Mode except `test` | Ignored | | `.env.development` | Variables for development | Committed | | `.env.test` | Variables for tests, loaded when `APP_ENV=test` | Committed | | `.env.{mode}.local` | Local overrides for one Mode, such as `.env.test.local` | Ignored | | `.env.example` | Every variable the App reads, with placeholder values | Committed | When a variable is defined in several files, the file with the higher priority wins. For `APP_ENV=development`, from the highest priority: 1. `.env.development.local` 2. `.env.local` 3. `.env.development` 4. `.env` For `APP_ENV=test`: 1. `.env.test.local` 2. `.env.test` 3. `.env` `.env.local` isn't loaded in `test`, so your local overrides can't change what the test suite sees. A variable that's already in the real environment wins over every file, so your platform or CI can override a value without editing a file. A variable set to an empty value, such as `SENTRY_DSN=`, still replaces the same variable from a lower priority file, and then counts as missing. :::warning In production, `env.Load` reads no `.env` file at all, not even `.env.production`. A `.env` copied into a production image is ignored, and the App fails at boot with every variable it expected from that file listed as missing. Set production configuration in the real environment instead: ```sh APP_ENV=production APP_URL=https://blog.example.com go run . ``` ::: `APP_ENV` itself must come from the real environment, because it decides which files to read; setting it in a `.env` file is reported. Read the Mode with `e.Mode()`, which returns `env.Development`, `env.Test` or `env.Production`. Code that reads `APP_ENV` without an Env, such as a script's own check, parses it with `env.ParseMode`, the rule every part of Tinka uses: the three Modes in lowercase, and development when it's empty. `env.Load` reads the files from the working directory. Set `ENV_PATH` in the real environment to read them from another directory, resolved against the working directory: ```sh ENV_PATH=config/env go run . ``` In code, `env.Dir` sets the directory `ENV_PATH` is resolved against: ```go e := env.Load(env.Dir("deploy/local")) ``` `.env.example` documents the App's variables for your team. Commit it with placeholder values, so a new developer copies it to `.env` and fills in their own: ```dotenv title=".env.example" PORT=3333 APP_URL=http://localhost:3333 APP_KEY=your_app_key MAIL_FROM=hello@example.com STRIPE_KEY=your_stripe_test_key ``` ### Validating environment variables Every getter both reads and validates. A getter without a fallback reads a required variable, and `Err` reports it when it's missing. Pass a fallback to make the variable optional; the getter returns the fallback when the variable is missing. An empty value counts as missing. A value that's present but invalid is always reported, even with a fallback, because it's a mistake you want to see. ```go e.Int("PORT") // required e.Int("PORT", 3333) // optional, 3333 when missing ``` Each getter converts to one type: ::::options :::option{name="String" type="func(key string, fallback ...string) string"} The value as it is. ::: :::option{name="Int" type="func(key string, fallback ...int) int"} A whole number in base 10, such as `3333`. ::: :::option{name="Float" type="func(key string, fallback ...float64) float64"} A finite number, such as `0.25`. ::: :::option{name="Bool" type="func(key string, fallback ...bool) bool"} `true` or `false`, also `1`, `0`, `t`, `f`, and their upper and title case forms. ::: :::option{name="Duration" type="func(key string, fallback ...time.Duration) time.Duration"} A `time.Duration` written as `30s`, `5m` or `1h30m`. A number without a unit is refused. ::: :::option{name="URL" type="func(key string, fallback ...string) *url.URL"} An absolute URL with a scheme and a host, such as `https://blog.example.com`. ::: :::option{name="Host" type="func(key string, fallback ...string) string"} A hostname or an IP address, such as `db.internal` or `10.0.0.5`. ::: :::option{name="Email" type="func(key string, fallback ...string) string"} A bare email address, such as `hello@example.com`. ::: :::option{name="UUID" type="func(key string, fallback ...uuid.UUID) uuid.UUID"} A UUID, returned as a `uuid.UUID`. ::: :::option{name="OneOf" type="func[T ~string](key string, choices []T, fallback ...T) T"} One of `choices`, returned as their own string type, so the result needs no conversion: ```go type LogFormat string format := e.OneOf("LOG_FORMAT", []LogFormat{"text", "json"}, "text") ``` ::: :::option{name="Secret" type="func(key string, fallback ...string) env.Secret"} The value as an `env.Secret`, for API keys and passwords. Printing it with `fmt`, logging it with `log/slog` and encoding it as JSON all show `[redacted]`, so it can't leak through a log line or an error message. Call `Reveal` only where the value is used: ```go key := e.Secret("STRIPE_KEY") slog.Info("payments ready", "key", key) // key=[redacted] client := stripe.New(key.Reveal()) ``` Pass `""` as the fallback to make a secret optional. Once revealed, the plain string is no longer protected. ::: :::: A problem quotes the rejected value so you can fix it, except from `String`, `Secret` and `URL`, which can hold credentials. To require a variable in one Mode only, branch on the Mode: ```go dsn := e.Secret("SENTRY_DSN", "") if e.Mode() == env.Production { dsn = e.Secret("SENTRY_DSN") } ``` `env.Map` builds an Env from a map, without reading the process environment or any file. Tests that use it can run in parallel, which `t.Setenv` doesn't allow: ```go title="config_test.go" package main import ( "testing" "github.com/mucolabs/tinka/env" ) func TestPortFallback(t *testing.T) { t.Parallel() e := env.Map(map[string]string{"APP_ENV": "test"}) if got := e.Int("PORT", 3333); got != 3333 { t.Errorf("PORT = %d, want 3333", got) } } ``` To test a function like `loadConfig` this way, have it take the Env as a parameter, and pass `env.Load()` from `main`. ### Writing .env files from a Command A Command that generates a value, such as a new `APP_KEY`, records it with an `env.Editor`. `env.Edit` opens `.env` and `.env.example` in the directory `env.Load` reads from, and `Save` writes them back with every other line, comment and blank line unchanged: ```go title="commands/key.go" package commands import "github.com/mucolabs/tinka/env" // SaveKey records key as the App's APP_KEY. func SaveKey(key string) error { ed, err := env.Edit() if err != nil { return err } if err := ed.SetSecret("APP_KEY", key); err != nil { return err } return ed.Save() } ``` `Set` writes the value to both files. `SetSecret` writes it to `.env` and leaves it empty in `.env.example`, so the example names the variable without revealing it. An existing key is updated in place and a new one is added at the end. The value is quoted when it needs to be, so `env.Load` reads it back exactly. A file that doesn't exist is never created; `ed.Files()` lists the ones found. To read a `.env` file without loading it, `env.Parse` returns its variables with references between them expanded, and `env.Paths` lists the files `env.Load` reads for a Mode, from the highest priority. See also: [Commands](reference/commands.md#act-generatekey), [Installation](installation.md) --- Source: https://tinka.tmuco.dev/deployment.md # Deployment > Build the App into one static binary with act build, run it in production, and package it in a container. This guide covers deploying a Tinka App. You will learn how to: - Build the App for production with `act build` - Ship static files inside the binary - Build for another operating system or architecture - Run the binary in production - Package the App in a container image ## Overview A Tinka App builds into **one binary** that holds everything it needs: your code, Tinka, and any files you embed. There's nothing to install next to it, no runtime and no dependencies to copy, so deploying the App means copying one file and running it with its configuration in the environment. `act build` runs `go build` with the settings a production binary needs, and prints the command it runs, so a machine without `act`, such as a CI runner or a container build, runs the same command with Go alone. ## Build the App Run `act build` from the App's directory: ```sh act build ``` ```text title="Output" $ GOWORK=off CGO_ENABLED=0 go build -trimpath -o bin/blog . Built bin/blog (14.7 MB, v1.2.0, linux/amd64, revision 58d23ba966e3) ``` The binary is named after the last part of the App's module path and written to `bin/`. It is built with: - `CGO_ENABLED=0`, so it's **static**: it runs on any machine of the same operating system and architecture, including minimal container images without a C library. - `-trimpath`, so it holds no paths from the machine that built it, and the same commit builds the same binary anywhere. - `GOWORK=off`, so a `go.work` file on your machine can't change what goes in. The version comes from the git tag of the commit, such as `v1.2.0`, and ends in `+dirty` when the App has uncommitted changes. Go records it in the binary, along with the commit, so `act about` and your own code read it without a build step. `act build` warns when `go.mod` replaces a module with a local directory, because that build differs from one made from the same commit on another machine, and when the App has uncommitted changes. :::tip Add `bin/` to `.gitignore`. Git counts files it doesn't track as changes, so a `bin/` it sees makes every build after the first one `+dirty`. ::: ## Ship static files Files the App serves, such as stylesheets and images, go inside the binary with `embed`, so they always match the code that serves them: ```go title="assets.go" package main import ( "embed" "io/fs" "net/http" "github.com/mucolabs/tinka" ) //go:embed public var public embed.FS // assets serves the files in public/ under /assets/. func assets(app *tinka.App) error { files, err := fs.Sub(public, "public") if err != nil { return err } app.Handle(http.MethodGet, "/assets/", http.StripPrefix("/assets/", http.FileServerFS(files))) return nil } ``` `act serve` rebuilds the App when an embedded file changes, as it does for Go files. ## Build for another system Pass `--target` to build for another operating system and architecture, such as a Linux server from a Mac: ```sh act build --target linux/arm64 ``` The first build for a new target takes longer, while Go builds its standard library for it; later builds are as fast as usual. Pass `--cgo` only when a dependency needs C, such as a SQLite driver written in C: the binary then needs the system's C library at run time. ## Run the App in production Copy the binary to the server, and run it with its configuration in the environment. Production reads no `.env` file, so set every variable in your platform's settings or your service definition: ```sh APP_ENV=production APP_KEY=your_app_key ./blog ``` Run with no arguments, the binary serves requests; with arguments, it runs one of the App's [Commands](guides/command-line/introduction.md), such as `./blog about`. It shuts down gracefully on `SIGTERM`, which is what platforms send when they replace an instance. See [Application lifecycle](guides/concepts/application-lifecycle.md) for the shutdown timeout and [Health checks](guides/digging-deeper/health-checks.md) for readiness probes and draining. On a server managed with systemd, socket activation keeps the port open while the App restarts. See [Socket activation](guides/concepts/application-lifecycle.md#socket-activation). ## Package the App in a container A container build has Go but not `act`, so it runs the `go build` command that `act build` prints. Build in a Go image, then copy the static binary into a minimal one: ```dockerfile title="Dockerfile" FROM golang:1.27 AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -trimpath -o /blog . FROM gcr.io/distroless/static-debian12 COPY --from=build /blog /blog ENV APP_ENV=production USER nonroot EXPOSE 3000 ENTRYPOINT ["/blog"] ``` A `.dockerignore` that leaves out `.git` also leaves out the version: the binary then reports `(devel)`. Copy `.git` into the build stage when you want the version recorded. See also: [Configuration & Environment](configuration.md), [Commands reference](reference/commands.md#act-build), [Health checks](guides/digging-deeper/health-checks.md) --- Source: https://tinka.tmuco.dev/faqs.md # FAQs > Short answers to common questions about Tinka: stability, the Go version, net/http compatibility, dependencies, databases, the docs for agents and the license. These are the questions people and agents ask most before building with Tinka, with short answers and links to the pages that explain more. ## Is Tinka ready for production? Not yet. Tinka is in early alpha, and its APIs will change before `v1.0.0`. You can build and deploy an App today, but expect to update your code between releases, and pin the version you build against in `go.mod`. [Status and stability](introduction.md#status-and-stability) lists what ships. ## Which Go version does Tinka need, and why? Go 1.27 or later. Tinka relies on two parts of it: generic methods, which let a route such as `app.Get` take a Handler that returns any type and send that type as the response, and `encoding/json/v2`, which decodes request bodies and encodes JSON responses. Check your version with `go version`. ## Can I use my existing net/http handlers and middleware? Yes. `app.Handle` registers any `http.Handler`, such as `http.FileServerFS`, next to your routes, and `tinka.Std` adapts middleware with the `func(http.Handler) http.Handler` shape, such as a CORS or compression package, so it runs in the chain like any other. An App is itself an `http.Handler`, so another server or a test can run it directly. See [serving other handlers](guides/basics/routing.md#serve-other-handlers) and [using net/http middleware](guides/basics/middleware.md#use-nethttp-middleware). ## Does Tinka use reflection or code generation? No code generation step runs before `go build`, and nothing wraps `go test` or `go run`. The `act make` [generators](guides/concepts/scaffolding.md) write ordinary Go files once, which you then own and edit like any other code. The [dependency injection](guides/concepts/dependency-injection.md) container uses no reflection: each binding is a typed factory, keyed by its Go type, so the compiler checks the wiring. The [body parser](guides/basics/body-parser.md) reads struct tags to decode form fields into your types, the way `encoding/json` reads `json` tags. ## What does Tinka depend on? The standard library and three modules maintained by the Go team: `golang.org/x/crypto` for password hashing, `golang.org/x/sys` and `golang.org/x/term`. Nothing else reaches your App. The tools that lint Tinka and build its docs live in a separate module, so their dependencies never appear in your `go.sum`. ## Can I use one Tinka package without the rest? Yes. Packages such as `env`, `log`, `password`, `encryption`, `events`, `health`, `di` and `cli` don’t import the rest of the framework, and Go compiles only the packages you import. Add `github.com/mucolabs/tinka` to your module with `go get` and import the package you need. ## How do I use a database today? Tinka has no data layer yet, so you use `database/sql` with the driver of your choice. Register the `*sql.DB` connection pool as a singleton in a [service provider](guides/concepts/service-providers.md), and check the database is reachable in its `Boot` hook. The container closes the pool when the App shuts down, because `*sql.DB` has a `Close` method. Handlers get the pool from the container with `c.Make`, and `health.Ping` turns `db.PingContext` into a readiness check, as [health checks](guides/digging-deeper/health-checks.md) shows. ## What is the difference between tinka and act? The Tinka CLI, `tinka`, creates Apps and needs no App to run. `act` works inside an App’s directory: it serves the App while you work, runs its tests, builds it for production and runs its Commands, including the ones you write. [Installation](installation.md) installs both. ## How do agents read the docs? Every docs page has a Markdown twin at its address plus `.md`, such as `/introduction.md`. `llms.txt` at the root of the docs site lists every page with its description and the address of its twin, and `llms-full.txt` holds every twin in one file. A request that accepts `text/markdown` gets the twin at the page’s own address. Commands answer agents too, with one JSON result per run: see [Agent mode](reference/agent-mode.md). ## Why does go install fail during the alpha? The Tinka repository is private during the alpha, so Go’s public module proxy can’t fetch it, and `go install` fails with a 404 or an authentication error. Set `GOPRIVATE` so Go fetches Tinka directly from GitHub, and make sure `git` can authenticate to GitHub with an account that has access: ```sh go env -w GOPRIVATE=github.com/mucolabs/* ``` Check the same setting when `tinka new` creates the App but can’t download its dependencies. See [Installation](installation.md). ## What license is Tinka released under? The MIT License. You can use Tinka in commercial and open source Apps, as long as you keep the copyright and license notice. See also: [Introduction](introduction.md), [Philosophy](philosophy.md), [Pick your path](pick-your-path.md) --- Source: https://tinka.tmuco.dev/tutorial/overview.md # Tutorial overview > Build one Tinka App from an empty directory to a deployed binary: a JSON API for a blog's posts, with a Command and tests. This tutorial builds one App from an empty directory to a binary running in production: a JSON API for a blog's posts. Each chapter adds to the code of the one before, and every command and output it shows comes from building that App. You will learn how to: - Create an App from the skeleton starter kit and run it while you work - Add routes whose handlers return values, and answer with errors clients can act on - Read and validate JSON request bodies - Share a service through the container with a service provider - Add a Command that people and agents run - Test the routes and the Command - Build the App into one binary and run it in production ## What you will build The App is called `blog`. By the last chapter it answers three routes, runs one Command of its own, and has tests for all of them: | Request or Command | What it does | |---|---| | `GET /posts` | Lists every post | | `GET /posts/{id}` | Shows one post, or answers `404 Not Found` | | `POST /posts` | Creates a post from a JSON body, or answers `422` naming each invalid field | | `act posts:list` | Lists the posts in the terminal, or as JSON for an agent | Tinka has no database package yet, so the App keeps its posts in memory, in a small `Store` type of its own. They last until the App stops. The store's methods take a context and return errors, the way a store backed by a database does, so a data layer can replace it later without changing the handlers or the Command that use it. ## What you need You need Go 1.27 or later, and the `tinka` and `act` commands, which the first chapter installs. Check your Go version with `go version`. The tutorial sends requests with `curl` and assumes you can read Go: structs, methods and error handling. ## The chapters Work through them in order, since each one starts from the code the previous one ended with: 1. [Create the App](create-the-app.md): install the tools, create the App from the skeleton starter kit, and run it with `act serve`, which rebuilds it on every save. 2. [Routes and handlers](routes-and-handlers.md): keep the posts in a store, and add `GET /posts` and `GET /posts/{id}` with handlers that return values and a `404` for a missing post. 3. [Request bodies](request-bodies.md): add `POST /posts`, which decodes and validates a JSON body and answers `201 Created`. 4. [Services and providers](services-and-providers.md): move the store into the App's container with a service provider, so every part of the App shares one. 5. [Commands](commands.md): add `posts:list`, a Command that uses the store from the container, and run it as a person and as an agent. 6. [Testing](testing.md): test the routes with `tinkatest` and the Command with `clitest`, and run them with `act test`. 7. [Deploy](deploy.md): build the App with `act build`, and run the binary with its production environment and health checks. ## How code is shown The first time a file appears, it's shown whole, with its path above it, so you can copy it as it is. When a later step changes a file, only the change is shown, with added and removed lines marked, and enough lines around them to find the place. Commands you run are in shell blocks, and what they print follows in a block titled `Output`. See also: [Installation](../installation.md), [Folder structure](../folder-structure.md), [Introduction](../introduction.md) --- Source: https://tinka.tmuco.dev/tutorial/create-the-app.md # Create the App > Install tinka and act, create the blog App from the skeleton starter kit, and run it with act serve, which rebuilds it on every save. This chapter creates the App the rest of the tutorial builds on. You will learn how to: - Install the Tinka CLI and `act` - Create an App from the skeleton starter kit - Run the App with `act serve` and send it a request - Read what `act serve` does when you save a change ## Overview The skeleton starter kit gives a new App the structure every later chapter relies on: configuration read from the environment, one place for routes, and tests ready to run. `act serve` then runs the App and builds it again each time you save a file, so you start it once, in a terminal of its own, and keep it running while you work. ## Install the tools Tinka has two commands, both installed with Go's own tooling. `tinka` creates Apps, and `act` runs Commands inside an App, such as `act serve`. ::::steps :::step{title="Install the Tinka CLI"} ```sh go install github.com/mucolabs/tinka/cmd/tinka@latest ``` ::: :::step{title="Install act"} ```sh go install github.com/mucolabs/tinka/cmd/act@latest ``` ::: :::: Go places both binaries in `$(go env GOPATH)/bin`, which must be on your `PATH`. [Installation](../installation.md) shows how to check them, and the setting Go needs during the alpha, while the Tinka repository is private. ## Create the App Run `tinka new` with the directory to create. It asks which starter kit to use: choose `skeleton`. Answer yes when it asks whether to initialize a git repository, since the last chapter versions the App from its git tags. ```sh tinka new blog ``` ```text title="Output" Copying the skeleton kit ............................................ 5ms DONE Installing dependencies ............................................ 70ms DONE Initializing a git repository ....................................... 9ms DONE Created blog with the skeleton kit cd blog act serve ``` `tinka new` copies the kit, requires the version of Tinka it was installed at, downloads the dependencies and generates the App's `APP_KEY` into `.env`. It also trusts the new directory, so `act` builds and runs the App's code without asking first. Move into the App: ```sh cd blog ``` ## Look around the App The skeleton puts each kind of code in its own package. These are the ones the tutorial changes: | Path | Holds | |---|---| | `main.go` | The entry point: it loads the configuration and runs the App | | `start/app.go` | `App`, which builds the App and adds its services and Commands | | `start/routes.go` | `Routes`, which registers the App's routes | | `app/handlers` | The handlers that answer requests | | `config` | The configuration, read from the environment at boot | | `tests` | `tests.App`, which builds the App for tests, and the tests themselves | [Folder structure](../folder-structure.md) describes every file. For now, two matter. `start/routes.go` registers the home route, and the health check routes the last chapter uses: ```go title="start/routes.go" package start import ( "github.com/mucolabs/tinka" "github.com/mucolabs/tinka/health" "blog/app/handlers" ) // Routes registers the App's routes. It runs as the App starts, after the // providers registered their services, so it can resolve them; an error // stops the App before it serves. func Routes(app *tinka.App) error { app.Get("/", handlers.Home) // GET /health/live and GET /health/ready, for load balancers. Add a // check for each service the App can't serve without, such as its // database. app.Health(health.New(health.DiskSpace("/"))) return nil } ``` The home route's handler returns a struct, which the App sends as JSON: ```go title="app/handlers/home.go" // Package handlers holds the App's handlers: the functions that answer // requests. package handlers import "github.com/mucolabs/tinka" type greeting struct { Hello string `json:"hello"` } // Home answers GET / with JSON. func Home(c *tinka.Ctx) (greeting, error) { return greeting{Hello: "world"}, nil } ``` ## Run the App Start the dev server from the App's directory, in a terminal you leave open: ```sh act serve ``` ```text title="Output" INFO Watching for changes (inotify, 8 directories) INFO Built in 1.3s, serving http://localhost:3000 [01:07:43.106] INFO: server started addr=127.0.0.1:35265 took=694.584µs ``` The lines starting with `INFO` come from `act serve`, and the timestamped ones from the App's own logger. `act serve` listens on `localhost:3000` and forwards each request to the App, which listens on a private port of its own: that's the address the App logs. In another terminal, request the home route: ```sh curl localhost:3000/ ``` ```text title="Output" {"hello":"world"} ``` The dev server's terminal logs the request, with its status, its duration and the request's ID: ```text title="Output" [01:07:43.123] INFO: request method=GET path=/ status=200 took=83.306µs request_id=T2BGTXNW3JFUQLWPTFHY3S6PJW ``` ## Save a change `act serve` builds the App again each time you save a Go file, while the running App keeps answering. Make a mistake on purpose to see what a failed build does: remove `, nil` from the handler's `return`, and save. ```go title="app/handlers/home.go" del={3} ins={4} // Home answers GET / with JSON. func Home(c *tinka.Ctx) (greeting, error) { return greeting{Hello: "world"}, nil return greeting{Hello: "world"} } ``` The build fails, and the dev server prints the compiler's error: ```text title="Output" ERROR The build failed after 149ms; the App keeps running its previous code # blog/app/handlers app/handlers/home.go:13:9: not enough return values have (greeting) want (greeting, error) ``` `curl` still gets `{"hello":"world"}` from the App that was running before the change, so a typo never takes the App down. A page you open in the browser shows the error instead, where you're looking. Put `, nil` back and save again. The new build replaces the old App in a few milliseconds: ```text title="Output" [01:08:03.974] INFO: server stopped INFO Built in 965ms, restarted in 34ms [01:08:04.015] INFO: server started addr=127.0.0.1:35265 took=805.55µs ``` Requests that arrive during the restart wait for the new App instead of failing. Keep `act serve` running: every change in the next chapters takes effect when you save it. ## Recap You installed `tinka` and `act`, created the `blog` App from the skeleton starter kit, and ran it with `act serve`, which rebuilds and restarts it on every save and keeps the old App serving when a build fails. Next, [Routes and handlers](routes-and-handlers.md) gives the App its first posts. See also: [Installation](../installation.md), [Folder structure](../folder-structure.md), [Development environment setup](../development.md), [Commands reference](../reference/commands.md#act-serve) --- Source: https://tinka.tmuco.dev/tutorial/routes-and-handlers.md # Routes and handlers > Give the blog App a store of posts, and add GET /posts and GET /posts/{id} with handlers that return values and a 404 for a missing post. This chapter adds the App's first routes: one that lists the posts, and one that shows a post by its ID. You will learn how to: - Keep the posts in a store type of their own, with no HTTP in it - Write handlers that return values instead of writing responses - Register routes, and read a path parameter - Answer `404 Not Found` for a post that doesn't exist ## Overview A route connects a method and a path, such as `GET /posts`, to a handler. A Tinka handler is a function that receives the request's `*tinka.Ctx` and returns two things: a value, which the App sends as the response, and an error, which the App turns into an error response. A handler never writes the response itself, so it reads top to bottom and can't answer twice. The posts themselves don't belong in the handlers. They go in a package of their own, `app/posts`, which knows nothing about HTTP: a **Capability**, in Tinka's terms. The handlers adapt requests to it, and later chapters reuse it from a Command and from tests without going through HTTP. ## Store the posts `Store` keeps the posts in a slice, in memory. Its methods take a `context.Context` and return an error even though memory never fails, because a store backed by a database needs both: when one replaces this store, its callers don't change. `Find` returns `ErrNotFound` for an ID no post has, so callers can tell a missing post from a failure. `NewStore` starts the store with two posts, so the App has something to show. Create the file: ```go title="app/posts/posts.go" // Package posts keeps the blog's posts. package posts import ( "context" "errors" "slices" ) // ErrNotFound means no post has the ID asked for. var ErrNotFound = errors.New("post not found") // Post is one blog post. type Post struct { ID int64 `json:"id"` Title string `json:"title"` Body string `json:"body"` } // Store keeps the posts in memory, so they last until the App stops. Its // methods take a context and return errors, as a store backed by a // database does, so one can replace it without changing its callers. type Store struct { posts []Post } // NewStore returns a Store holding two posts, so the App has something to // show. func NewStore() *Store { return &Store{posts: []Post{ {ID: 1, Title: "Hello, Tinka", Body: "The first post on the blog."}, {ID: 2, Title: "Routes and handlers", Body: "How a request finds its handler."}, }} } // All returns every post, oldest first. func (s *Store) All(ctx context.Context) ([]Post, error) { return slices.Clone(s.posts), nil } // Find returns the post with the ID id, or ErrNotFound. func (s *Store) Find(ctx context.Context, id int64) (Post, error) { for _, p := range s.posts { if p.ID == id { return p, nil } } return Post{}, ErrNotFound } ``` `All` returns a copy of the slice, so a caller that changes what it received can't change the store. ## Write the handlers The handlers need the store, so they are methods on a `Posts` struct that holds it. A method value, such as `postHandlers.List`, is a function of the right shape, so the router takes it as a handler. `List` returns the store's posts as they are: a slice becomes a JSON array. `Show` reads the `{id}` path parameter with `c.ParamInt`, which parses it as a whole number and returns an error that answers `422` when it isn't one, so the handler returns that error unchanged. It then maps `posts.ErrNotFound` to `tinka.NotFound`, which answers `404` with the message you give it. Any other error passes through, and the App answers it with a generic `500` and logs it, so a store's internal error never reaches the client. The `*tinka.Ctx` is also the request's `context.Context`, so the handlers pass `c` to the store: when the client disconnects, a database query made with it stops. ```go title="app/handlers/posts.go" package handlers import ( "errors" "github.com/mucolabs/tinka" "blog/app/posts" ) // Posts answers the /posts routes from the store. type Posts struct { Store *posts.Store } // List answers GET /posts with every post. func (h Posts) List(c *tinka.Ctx) ([]posts.Post, error) { return h.Store.All(c) } // Show answers GET /posts/{id} with one post, or 404 Not Found. func (h Posts) Show(c *tinka.Ctx) (posts.Post, error) { id, err := c.ParamInt("id") if err != nil { return posts.Post{}, err } post, err := h.Store.Find(c, id) if errors.Is(err, posts.ErrNotFound) { return posts.Post{}, tinka.NotFound("No post has the ID %d.", id) } return post, err } ``` ## Register the routes `Routes` runs once, when the App starts. It creates the store there and gives it to the handlers, so every request shares the same store. `{id}` in a pattern matches one path segment, which `c.ParamInt("id")` reads. Add the import and the routes: ```go title="start/routes.go" ins={8,17-19} package start import ( "github.com/mucolabs/tinka" "github.com/mucolabs/tinka/health" "blog/app/handlers" "blog/app/posts" ) // Routes registers the App's routes. It runs as the App starts, after the // providers registered their services, so it can resolve them; an error // stops the App before it serves. func Routes(app *tinka.App) error { app.Get("/", handlers.Home) postHandlers := handlers.Posts{Store: posts.NewStore()} app.Get("/posts", postHandlers.List) app.Get("/posts/{id}", postHandlers.Show) ``` ## Try the routes Save the files, and `act serve` restarts the App with the new routes. List the posts: ```sh curl localhost:3000/posts ``` ```text title="Output" [{"id":1,"title":"Hello, Tinka","body":"The first post on the blog."},{"id":2,"title":"Routes and handlers","body":"How a request finds its handler."}] ``` Show the first one: ```sh curl localhost:3000/posts/1 ``` ```text title="Output" {"id":1,"title":"Hello, Tinka","body":"The first post on the blog."} ``` Ask for a post that doesn't exist, with `-i` to see the status and headers: ```sh curl -i localhost:3000/posts/7 ``` ```text title="Output" HTTP/1.1 404 Not Found Cache-Control: no-store Content-Length: 129 Content-Type: application/problem+json Date: Tue, 20 Oct 2026 23:10:49 GMT Referrer-Policy: strict-origin-when-cross-origin X-Content-Type-Options: nosniff X-Request-Id: VU2XFXBNWVRMBJK4VM6PJRTSCK {"title":"Not Found","status":404,"detail":"No post has the ID 7.","code":"not_found","request_id":"VU2XFXBNWVRMBJK4VM6PJRTSCK"} ``` The error became **problem details**, the standard JSON shape for HTTP errors (RFC 9457). `detail` holds your message, and `code` is a stable identifier a client can match on. `request_id` matches the `X-Request-Id` header and the dev server's log, which records the rejected request at `WARN`: ```text title="Output" [01:10:49.604] WARN: request rejected method=GET path=/posts/7 status=404 code=not_found detail="No post has the ID 7." request_id=VU2XFXBNWVRMBJK4VM6PJRTSCK ``` An ID that isn't a number never reaches the store. `c.ParamInt` refuses it, and the response names the parameter: ```sh curl localhost:3000/posts/latest ``` ```text title="Output" {"title":"Unprocessable Entity","status":422,"detail":"The path parameter id must be a whole number.","code":"invalid","fields":{"id":"must be a whole number"},"request_id":"FG4OZAWULJBCGQ2WVCVABVRISP"} ``` ## Recap The posts live in a `Store` in `app/posts`, with no HTTP in it. The handlers in `app/handlers/posts.go` return values that become JSON, and errors that become problem details: `tinka.NotFound` for a missing post, and the `422` that `c.ParamInt` returns for an ID that isn't a number. Next, [Request bodies](request-bodies.md) lets clients create posts. See also: [Routing](../guides/basics/routing.md), [HTTP context](../guides/basics/http-context.md), [Response](../guides/basics/response.md), [Exception handling](../guides/basics/exception-handling.md) --- Source: https://tinka.tmuco.dev/tutorial/request-bodies.md # Request bodies > Add POST /posts to the blog App, decoding a JSON body with c.Body, validating every field at once, and answering 201 Created. This chapter lets clients create posts by sending them as JSON. You will learn how to: - Make the store safe for handlers that run at the same time - Decode a request body into a typed value with `c.Body` - Validate the body and report every invalid field in one response - Answer `201 Created` with the new post ## Overview `c.Body[T]()` reads the request body into a value of the type you name, picking the format from the request's `Content-Type`. When the type has a `Valid` method, `c.Body` calls it after decoding. Every failure, from a body that isn't JSON to a title that's missing, comes back as an error that's already the right response, so the handler returns it unchanged. Creating posts also changes the store for the first time. Until now, requests only read it; from here on, two requests can write to it at the same moment. ## Let the store create posts The App runs handlers concurrently, one goroutine per request, so two `POST /posts` requests can append to the slice at the same time and lose one of the posts. A `sync.RWMutex` lets any number of requests read the posts together, and gives a writer the store to itself. Import `sync`: ```go title="app/posts/posts.go" ins={5} import ( "context" "errors" "slices" "sync" ) ``` Add the mutex to the store: ```go title="app/posts/posts.go" ins={2} type Store struct { mu sync.RWMutex // handlers run concurrently posts []Post } ``` Lock it for reading in `All` and `Find`, and add `Create`, which locks it for writing and gives the post the next ID: ```go title="app/posts/posts.go" ins={3-4,10-11,20-27} // All returns every post, oldest first. func (s *Store) All(ctx context.Context) ([]Post, error) { s.mu.RLock() defer s.mu.RUnlock() return slices.Clone(s.posts), nil } // Find returns the post with the ID id, or ErrNotFound. func (s *Store) Find(ctx context.Context, id int64) (Post, error) { s.mu.RLock() defer s.mu.RUnlock() for _, p := range s.posts { if p.ID == id { return p, nil } } return Post{}, ErrNotFound } // Create adds a post and returns it with its new ID. func (s *Store) Create(ctx context.Context, title, body string) (Post, error) { s.mu.Lock() defer s.mu.Unlock() post := Post{ID: int64(len(s.posts)) + 1, Title: title, Body: body} s.posts = append(s.posts, post) return post, nil } ``` ## Describe the body The handler decodes the body into a type of its own, `newPost`, that holds only what a client may send. A client can't choose a post's ID, because `newPost` has no field for it. JSON members match the `json` tags exactly, including case, and members the type doesn't have are ignored. `Valid` checks the decoded input and returns a problem for each invalid field, keyed by the field's name. An empty map means the input is valid. The title must hold something other than spaces and stay under 100 characters, counted with `utf8.RuneCountInString` so that "é" counts as one, and the body must not be empty. The `switch` reports at most one problem for the title, the one that applies. Add the packages `Valid` and the handler use to the imports: ```go title="app/handlers/posts.go" ins={2,4-6} import ( "context" "errors" "net/http" "strings" "unicode/utf8" "github.com/mucolabs/tinka" "blog/app/posts" ) ``` Then add the type, after `Show`: ```go title="app/handlers/posts.go" ins={1-20} // newPost is the body POST /posts accepts. type newPost struct { Title string `json:"title"` Body string `json:"body"` } // Valid reports each field the post can't be created with. func (p newPost) Valid(ctx context.Context) map[string]string { problems := map[string]string{} switch { case strings.TrimSpace(p.Title) == "": problems["title"] = "Enter a title." case utf8.RuneCountInString(p.Title) > 100: problems["title"] = "Keep the title under 100 characters." } if strings.TrimSpace(p.Body) == "" { problems["body"] = "Write the post's body." } return problems } ``` :::tip Keep `Valid` to checks on the input itself: required fields, lengths, formats. A rule that needs the store, such as "no two posts share a title", belongs in the store's `Create`, which returns an error the handler maps to a response, as `Show` does with `ErrNotFound`. ::: ## Create the post `Create` asks for the body as a `newPost`. A body that can't be decoded or isn't valid returns an error, and the handler returns it. Otherwise the store creates the post, `c.Status` sets the status to `201 Created`, and the handler returns the post with its new ID. Add it at the end of the file: ```go title="app/handlers/posts.go" ins={1-13} // Create answers POST /posts with the new post and 201 Created. func (h Posts) Create(c *tinka.Ctx) (posts.Post, error) { in, err := c.Body[newPost]() if err != nil { return posts.Post{}, err } post, err := h.Store.Create(c, in.Title, in.Body) if err != nil { return posts.Post{}, err } c.Status(http.StatusCreated) return post, nil } ``` Register the route next to the others: ```go title="start/routes.go" ins={4} postHandlers := handlers.Posts{Store: posts.NewStore()} app.Get("/posts", postHandlers.List) app.Get("/posts/{id}", postHandlers.Show) app.Post("/posts", postHandlers.Create) ``` ## Try it Save the files, then create a post, with `-i` to see the status: ```sh curl -i -X POST localhost:3000/posts -H 'Content-Type: application/json' -d '{"title": "Request bodies", "body": "Reading JSON with c.Body."}' ``` ```text title="Output" HTTP/1.1 201 Created Content-Length: 69 Content-Type: application/json Date: Tue, 20 Oct 2026 23:11:17 GMT Referrer-Policy: strict-origin-when-cross-origin X-Content-Type-Options: nosniff X-Request-Id: 4A4L6IVQEC65FB73P5CPCDGDKD {"id":3,"title":"Request bodies","body":"Reading JSON with c.Body."} ``` The new post is in the store, at the ID it was given: ```sh curl localhost:3000/posts/3 ``` ```text title="Output" {"id":3,"title":"Request bodies","body":"Reading JSON with c.Body."} ``` Send a title of spaces and no body. Both problems come back in one `422`, under `fields`, so a client can show every error at once: ```sh curl -X POST localhost:3000/posts -H 'Content-Type: application/json' -d '{"title": " "}' ``` ```text title="Output" {"title":"Unprocessable Entity","status":422,"detail":"Some fields are invalid.","code":"invalid","fields":{"body":"Write the post's body.","title":"Enter a title."},"request_id":"IE6PQOK2R4WBPZWSQTZTEV4N6D"} ``` A body that isn't valid JSON never reaches `Valid`. It's a `400`, with a message that says what's wrong: ```sh curl -X POST localhost:3000/posts -H 'Content-Type: application/json' -d '{"title": "Request bodies",' ``` ```text title="Output" {"title":"Bad Request","status":400,"detail":"The request body ends in the middle of a JSON value.","code":"bad_json","request_id":"GSI3T3OETFCGGM7DIUEU6QXWPD"} ``` The same handler also accepts an HTML form, since `c.Body` decodes form fields by the same `json` tags. `curl -d` without a `Content-Type` header sends one: ```sh curl -X POST localhost:3000/posts -d 'title=From+a+form&body=Forms+work+too' ``` ```text title="Output" {"id":4,"title":"From a form","body":"Forms work too"} ``` :::note The store lives in the App's memory. Each time you save a file, `act serve` restarts the App with a new store holding the two starting posts, so the posts you created are gone and IDs start again from 3. ::: ## Recap The store locks itself, so concurrent requests can create posts safely. `Create` decodes the body with `c.Body[newPost]()`, whose `Valid` method reports every invalid field in one `422`, and answers `201 Created` with the new post. Next, [Services and providers](services-and-providers.md) moves the store into the App's container, so more than the routes can use it. See also: [Body parser](../guides/basics/body-parser.md), [Exception handling](../guides/basics/exception-handling.md), [Response](../guides/basics/response.md) --- Source: https://tinka.tmuco.dev/tutorial/services-and-providers.md # Services and providers > Move the blog App's post store into the container with a service provider created by act make:provider, and resolve it where the routes register. This chapter turns the post store into a service the whole App shares. You will learn how to: - Create a service provider with `act make:provider` - Bind the store in the App's container as a singleton - Resolve the store when the routes register - Tell what happens when a binding is missing ## Overview `Routes` creates the store today, so only the handlers can reach it. The next chapter adds a Command that lists the posts, and the chapter after it replaces the store in tests: both need the store without going through the routes. Every App has a **container**, from the `di` package, that knows how to build each of the App's services and how long each one lives. A **service provider** is where a service joins the App: its `Register` hook binds the service in the container, and its other hooks, all optional, run at the App's boot, start, ready and shutdown. Code anywhere in the App then asks the container for the service by its type, and gets the same one. ## Create the provider `act make:provider` writes a provider into `app/providers` and adds it to the App: ```sh act make:provider posts ``` ```text title="Output" CREATE app/providers/posts_provider.go UPDATE start/app.go + app.Provide(&providers.PostsProvider{}) ``` The generator added two lines to `start/app.go`: the import, and `app.Provide`, which hands the provider to the App before it boots: ```go title="start/app.go" ins={14,28} // Package start assembles the App: its configuration, services, // middleware and routes. main runs the App it builds, and tests build it // the same way. package start import ( "context" "github.com/mucolabs/tinka" "github.com/mucolabs/tinka/encryption" "github.com/mucolabs/tinka/log" "blog/app/exceptions" "blog/app/providers" "blog/config" ) // App builds the App from its configuration. func App(cfg config.Config) (*tinka.App, error) { enc, err := encryption.New(cfg.App.Key) if err != nil { return nil, err } cfg.HTTP.Logger = log.New(cfg.Logger) cfg.HTTP.Encrypter = enc app := tinka.New(cfg.HTTP) app.Provide(&providers.PostsProvider{}) app.OnError(exceptions.Handler) Kernel(app) app.OnStart(func(context.Context) error { return Routes(app) }) return app, nil } ``` ## Bind the store The generated provider has three empty hooks: `Register`, `Boot` and `Shutdown`. The store needs only `Register`, since memory needs no check at boot and nothing to release at shutdown. `Register` binds `*posts.Store` with `Singleton`: the factory you pass runs the first time something asks the container for a `*posts.Store`, and every later request gets that same store. The factory's return type is the binding's key, so the compiler checks the wiring, and there's no name to misspell. Replace the file's contents: ```go title="app/providers/posts_provider.go" package providers import ( "github.com/mucolabs/tinka/di" "blog/app/posts" ) // PostsProvider adds the posts store to the App. type PostsProvider struct{} // Register binds the store as a singleton: the App builds one, the first // time something asks for it, and every handler and Command shares it. func (p *PostsProvider) Register(c *di.Container) { c.Singleton(func(*di.Resolver) (*posts.Store, error) { return posts.NewStore(), nil }) } ``` When a database replaces the store, this provider is where it grows: `Register` binds the connection pool, and a `Boot` hook checks the database answers, so a misconfigured App stops at boot instead of failing its first request. ## Resolve the store in the routes `Routes` runs while the App starts, after every provider registered its bindings, so it can ask the container for the store. `app.Container()` returns the App's container, and `Make[*posts.Store]()` returns the store, building it on this first call, or an error when nothing provides it. `Routes` returns that error, which stops the App: ```go title="start/routes.go" del={1} ins={2-6} postHandlers := handlers.Posts{Store: posts.NewStore()} store, err := app.Container().Make[*posts.Store]() if err != nil { return err } postHandlers := handlers.Posts{Store: store} app.Get("/posts", postHandlers.List) ``` The handlers resolve nothing per request: they hold the store they were given, so a request pays nothing for the container. :::tip A store nothing provides stops the App as it starts, before it answers a single request. If you forget `app.Provide`, the App logs why and exits with status `1`: ```text title="Output" [01:48:14.099] ERROR: the App stopped err="tinka: start hook: di: cannot make *posts.Store: *posts.Store is not registered" ``` Add the provider back in `start/app.go` to fix it. ::: ## Check the App Save the files. The App behaves as before, now with its store from the container. Create a post, then list them: ```sh curl -X POST localhost:3000/posts -d 'title=Services+and+providers&body=The+store+moves+into+the+container.' curl localhost:3000/posts ``` ```text title="Output" {"id":3,"title":"Services and providers","body":"The store moves into the container."} [{"id":1,"title":"Hello, Tinka","body":"The first post on the blog."},{"id":2,"title":"Routes and handlers","body":"How a request finds its handler."},{"id":3,"title":"Services and providers","body":"The store moves into the container."}] ``` `act about` describes the App, and now lists the provider: ```sh act about ``` ```text title="Output" Environment Mode development Debug on Address :3000 Providers *providers.PostsProvider ``` The output above leaves out the first lines, which name the versions of Tinka and Go and your platform. ## Recap `PostsProvider` binds the store in the App's container as a singleton, and `start/app.go` adds the provider with `app.Provide`. `Routes` resolves the store with `Make` when the App starts, and a missing provider stops the App with an error before it serves anything. Next, [Commands](commands.md) uses the same store from the terminal. See also: [Service providers](../guides/concepts/service-providers.md), [Dependency injection](../guides/concepts/dependency-injection.md), [Application lifecycle](../guides/concepts/application-lifecycle.md), [Scaffolding](../guides/concepts/scaffolding.md) --- Source: https://tinka.tmuco.dev/tutorial/commands.md # Add a Command > Add posts:list to the blog App with act make:command, use the post store from the container, and return a result for people and agents. This chapter adds a Command that lists the blog's posts in the terminal. You will learn how to: - Create a Command with `act make:command` - Use the App's services from a Command - Return a result that people read as text and agents read as JSON - Run the Command with `act`, and in Agent mode ## Overview A **Command** is an action you run from the terminal. The App's own binary hosts its Commands: run with no arguments, it serves requests, and run with a Command's name, it runs that Command. `act` builds the binary and runs it for you, so `act posts:list` runs the App's `posts:list`. Before a Command runs, the App starts as it does for serving, without listening for requests: providers register their bindings, and the boot, start and ready hooks run. It shuts down once the Command returns. The Command then asks the container for the store, the same binding the routes use. ## Create the Command `act make:command` writes a Command into `app/commands`, named as you type it, and adds it to the App. The `posts:` part groups it with other Commands about posts: ```sh act make:command posts:list ``` ```text title="Output" CREATE app/commands/posts_list.go UPDATE start/app.go + app.Command(commands.PostsList(app)) ``` The generator imported the new package in `start/app.go`: ```go title="start/app.go" ins={1} "blog/app/commands" "blog/app/exceptions" "blog/app/providers" "blog/config" ``` It also added the Command to the App, after the provider: ```go title="start/app.go" ins={3} app := tinka.New(cfg.HTTP) app.Provide(&providers.PostsProvider{}) app.Command(commands.PostsList(app)) app.OnError(exceptions.Handler) ``` The generated Command already runs. It writes a status line and returns nothing: ```sh act posts:list ``` ```text title="Output" INFO posts:list ran ``` ## List the posts A Command doesn't print its result: its `Run` function returns it as a `cli.Result`, which is any type with a `String` method. People see what `String` returns. Agents, and anyone who passes `--json`, get the type's exported fields as JSON instead, so one Command serves both. `postList` holds the posts, with a `json` tag for the JSON key, and its `String` method writes one line per post. `PostsList` receives the App, so `Run` can ask its container for the store. `Make` returns an error instead of panicking, which `Run` returns as the Command's failure. `Run` passes its `ctx` to the store, as the handlers pass the request: it's cancelled when you press Ctrl+C. Replace the generated file's contents: ```go title="app/commands/posts_list.go" package commands import ( "context" "fmt" "strings" "github.com/mucolabs/tinka" "github.com/mucolabs/tinka/cli" "blog/app/posts" ) // postList is the posts:list Command's result. type postList struct { Posts []posts.Post `json:"posts"` } // String lists the posts for people, one per line. func (l postList) String() string { var b strings.Builder for _, p := range l.Posts { fmt.Fprintf(&b, "%4d %s\n", p.ID, p.Title) } return strings.TrimSuffix(b.String(), "\n") } // PostsList is the posts:list Command. It runs with the App started, // so it can use the App's services. func PostsList(app *tinka.App) *cli.Command { return &cli.Command{ Name: "posts:list", Summary: "List the blog's posts", Run: func(ctx context.Context, c *cli.Call) (cli.Result, error) { store, err := app.Container().Make[*posts.Store]() if err != nil { return nil, err } all, err := store.All(ctx) if err != nil { return nil, err } return postList{Posts: all}, nil }, } } ``` ## Run the Command Run it from the App's directory: ```sh act posts:list ``` ```text title="Output" 1 Hello, Tinka 2 Routes and handlers ``` The Command lists the two posts the store starts with, even when you created others through `act serve`. The Command runs in a process of its own, with its own App and its own store in memory. A store backed by a database would list the same posts in both, since both would read the same rows. `Summary` describes the Command in the App's list of Commands, which `act --help` prints: ```sh act --help ``` ```text title="Output" Run the App's Commands Usage: act [flags] Commands: about Describe the App: versions, Mode and what its providers configure generate:key Generate a random APP_KEY and write it to .env posts:list List the blog's posts completion Print a shell completion script Flags: -h, --help Show help --json Print a single JSON result instead of text --version Print the version ``` ## Run it as an agent When an agent, such as a coding assistant, runs a Command, it gets **Agent mode**: the Command never prompts, and prints a single JSON result. `act` detects agents on its own. Pass `--json` to see what they get: ```sh act posts:list --json ``` ```text title="Output" {"command":"posts:list","result":"ok","posts":[{"id":1,"title":"Hello, Tinka","body":"The first post on the blog."},{"id":2,"title":"Routes and handlers","body":"How a request finds its handler."}],"duration_ms":0} ``` `postList`'s `posts` field sits between the keys every result has: `command`, `result`, and `duration_ms`. A Command that fails answers with `"result":"error"` and an `error` object instead, so an agent never has to parse text written for people. ## Recap `posts:list` lives in `app/commands/posts_list.go`, and `start/app.go` adds it with `app.Command`. It runs with the App started, takes the store from the container, and returns a `postList` that people read as lines and agents read as JSON. Next, [Testing](testing.md) checks the routes and this Command with tests. See also: [Command line](../guides/command-line/introduction.md), [Creating Commands](../guides/command-line/commands.md), [Flags](../guides/command-line/flags.md), [Agent mode](../reference/agent-mode.md) --- Source: https://tinka.tmuco.dev/tutorial/testing.md # Test the App > Test the blog App's routes with tinkatest and its posts:list Command with clitest, swap the post store for a test, and run everything with act test. This chapter writes tests for everything the App does so far. You will learn how to: - Send requests to the App in a test with `tinkatest` - Assert on statuses, JSON bodies, problem details and invalid fields - Run a Command in a test with `clitest` - Give a test its own version of a service with `tinkatest.Fake` - Run the tests with `act test` and `go test` ## Overview A Tinka App's tests are Go tests: `TestXxx` functions in `_test.go` files, run by `go test`. The skeleton adds two helpers. `tests.App(t)` builds the App the way `main` does, in the `test` Mode, with the values in `.env.test`. `tinkatest.New` starts that App and returns a client whose requests go straight to the App's handler, without a network, so each one takes microseconds. Each test builds its own App, and each App has its own container, so each test gets its own store. A post one test creates never shows up in another, and every test can run in parallel with `t.Parallel()`. ## Test the routes `act make:test` creates an HTTP test in `tests/functional`: ```sh act make:test posts ``` ```text title="Output" CREATE tests/functional/posts_test.go ``` The generated test requests `GET /posts` and checks the status. Replace it with tests for each route. `Send(t)` sends a request and ties the response to the test, so a failed assertion reports the line that sent it, and each assertion returns the response, so they chain: - `AssertStatus` checks the status code. - `AssertJSONPath` checks one value in a JSON body, by keys and indexes joined with dots: `0.title` is the first post's title. - `AssertJSON` checks the whole body against a value, here a `posts.Post`, which it compares as JSON. - `AssertProblem` checks a problem details error by its status and its `code`. - `AssertInvalid` checks for a `422` that names each of the fields. Subtests are named as sentences that say what the route does, so a failure reads as the behavior that broke: ```go title="tests/functional/posts_test.go" package functional_test import ( "net/http" "testing" "github.com/mucolabs/tinka/tinkatest" "blog/app/posts" "blog/tests" ) func TestListPosts(t *testing.T) { t.Parallel() c := tinkatest.New(t, tests.App(t)) c.Get("/posts").Send(t). AssertStatus(http.StatusOK). AssertJSONPath("0.title", "Hello, Tinka"). AssertJSONPath("1.title", "Routes and handlers") } func TestShowPost(t *testing.T) { t.Parallel() t.Run("answers with the post", func(t *testing.T) { t.Parallel() c := tinkatest.New(t, tests.App(t)) c.Get("/posts/1").Send(t). AssertStatus(http.StatusOK). AssertJSON(posts.Post{ID: 1, Title: "Hello, Tinka", Body: "The first post on the blog."}) }) t.Run("answers 404 for a post that doesn't exist", func(t *testing.T) { t.Parallel() c := tinkatest.New(t, tests.App(t)) c.Get("/posts/99").Send(t).AssertProblem(http.StatusNotFound, "not_found") }) } func TestCreatePost(t *testing.T) { t.Parallel() t.Run("answers 201 with the new post", func(t *testing.T) { t.Parallel() c := tinkatest.New(t, tests.App(t)) c.Post("/posts").JSON(map[string]string{"title": "Testing", "body": "With tinkatest."}).Send(t). AssertStatus(http.StatusCreated). AssertJSONPath("id", 3) c.Get("/posts/3").Send(t).AssertJSONPath("title", "Testing") }) t.Run("refuses a post without a title or a body", func(t *testing.T) { t.Parallel() c := tinkatest.New(t, tests.App(t)) c.Post("/posts").JSON(map[string]string{"title": " "}).Send(t). AssertInvalid("title", "body") }) } ``` The create test sends a second request with the same client, which reaches the same App, so it proves the post was stored, not only returned. ## Test the Command The `clitest` package runs Commands in a test, in the same process, the way `act` runs them. `app.Commands()` returns the App's Commands, and `Run` runs one with its arguments and returns its exit code, its stdout and its stderr. To prove the Command lists what the container holds, the test gives the App a store of its own with a third post. `tinkatest.Fake` makes every `*posts.Store` the App resolves be the one you pass, until the test ends. Call it before the App starts: the Command starts the App when it runs. People's output ends with a newline after the result's `String`, so the expected text ends with one too. Tests of one package go next to it, so create the file in `app/commands`. It's in the `commands_test` package, which can import `blog/tests` even though that package imports `app/commands` itself: ```go title="app/commands/posts_list_test.go" package commands_test import ( "testing" "github.com/mucolabs/tinka/cli/clitest" "github.com/mucolabs/tinka/tinkatest" "blog/app/posts" "blog/tests" ) func TestPostsList(t *testing.T) { t.Parallel() store := posts.NewStore() if _, err := store.Create(t.Context(), "Testing", "Commands too."); err != nil { t.Fatal(err) } app := tests.App(t) tinkatest.Fake(t, app, store) r := clitest.New(t, app.Commands()).Run("posts:list") want := " 1 Hello, Tinka\n 2 Routes and handlers\n 3 Testing\n" if r.Code != 0 || r.Stdout != want { t.Errorf("exit %d, stdout %q, stderr %q", r.Code, r.Stdout, r.Stderr) } } ``` ## Run the tests `act test` runs every test of the App, and prints the `go test` command it runs first: ```sh act test ``` ```text title="Output" $ GOWORK=off go test -json ./... ? blog [no test files] ? blog/app/exceptions [no test files] ? blog/app/handlers [no test files] ? blog/app/posts [no test files] ? blog/app/providers [no test files] ? blog/config [no test files] ? blog/start [no test files] ? blog/tests [no test files] ok blog/app/commands 0.019s ok blog/tests/functional 0.017s 9 tests passed in 10 packages ``` The nine tests are `TestHome`, which came with the skeleton, the three test functions and four subtests in `posts_test.go`, and `TestPostsList`. The tests are Go tests, so `go test` runs them the same way, in your editor and in CI: ```sh go test ./... ``` ```text title="Output" ? blog [no test files] ok blog/app/commands 0.008s ? blog/app/exceptions [no test files] ? blog/app/handlers [no test files] ? blog/app/posts [no test files] ? blog/app/providers [no test files] ? blog/config [no test files] ? blog/start [no test files] ? blog/tests [no test files] ok blog/tests/functional 0.009s ``` Flags after `act test` go to `go test`, so `act test -run TestCreatePost ./tests/functional` runs one test, and `act test --watch` runs the tests again on every change. ## Read a failure A test's failure message should say what broke without opening the test. To see one, make the validation test also expect a `tags` field, which the App never reports: ```go title="tests/functional/posts_test.go" del={2} ins={3} c.Post("/posts").JSON(map[string]string{"title": " "}).Send(t). AssertInvalid("title", "body") AssertInvalid("title", "body", "tags") ``` Run the tests again: ```sh act test ``` ```text title="Output" $ GOWORK=off go test -json ./... ? blog [no test files] ok blog/app/commands (cached) ? blog/app/exceptions [no test files] ? blog/app/handlers [no test files] ? blog/app/posts [no test files] ? blog/app/providers [no test files] ? blog/config [no test files] ? blog/start [no test files] ? blog/tests [no test files] --- FAIL: TestCreatePost (0.00s) --- FAIL: TestCreatePost/refuses_a_post_without_a_title_or_a_body (0.00s) posts_test.go:56: POST /posts answered 422 Unprocessable Entity body: {"title":"Unprocessable Entity","status":422,"detail":"Some fields are invalid.","code":"invalid","fields":{"body":"Write the post's body.","title":"Enter a title."},"request_id":"WGZ6KP2QLCBOEGUFSJ5ODB74XL"} want errors for the fields title, body, tags, but there is none for tags FAIL FAIL blog/tests/functional 0.011s Error: 1 test failed. Fix: Run act test --failed to run only them again ``` The message names the request, the status and the body the App sent, then what the test wanted: the body has no `tags` field. `blog/app/commands` came from `go test`'s cache, since nothing it depends on changed. Remove `"tags"` again, and run only the test that failed: ```sh act test --failed ``` ```text title="Output" $ GOWORK=off go test -json -run ^(TestCreatePost)$ blog/tests/functional ok blog/tests/functional 0.009s 3 tests passed in 1 package ``` ## Recap `tests/functional/posts_test.go` tests each route through HTTP with `tinkatest`, and `app/commands/posts_list_test.go` runs `posts:list` with `clitest`, with a store of its own given by `tinkatest.Fake`. Each test builds its own App, so they all run in parallel. Next, [Deploy](deploy.md) builds the App for production. See also: [Testing](../guides/testing/introduction.md), [HTTP tests](../guides/testing/http-tests.md), [Testing Commands](../guides/testing/commands.md), [Test doubles](../guides/testing/test-doubles.md) --- Source: https://tinka.tmuco.dev/tutorial/deploy.md # Deploy the App > Build the blog App into one binary with act build, run it in production with its environment, probe its health checks, and run its Command from the binary. This chapter builds the App for production and runs it the way a server does. You will learn how to: - Version the App with a git tag, and build it with `act build` - Run the binary in the production Mode, with its configuration in the environment - Probe the health check routes the skeleton registers - Run the App's Commands from the binary, where `act` isn't installed - Stop the App gracefully ## Overview An App builds into one binary that holds everything it needs: your code, Tinka and the standard library. Deploying it means copying that file to a server and running it with its configuration in the environment. The same binary serves requests when it runs with no arguments, and runs a Command, such as `posts:list`, when it runs with one. ## Version the App `act build` records the App's version in the binary, from the git tag of the commit it builds, and marks a build with uncommitted changes as `+dirty`. Commit the App, then tag the commit: ```sh git add -A git commit -m "Build the blog API" git tag v0.1.0 ``` `.gitignore` came with the skeleton, so the commit leaves out `.env`, which holds your machine's `APP_KEY`, and `bin/`, where the build goes. ## Build the binary Run `act build` from the App's directory: ```sh act build ``` ```text title="Output" $ GOWORK=off CGO_ENABLED=0 go build -trimpath -o bin/blog . Built bin/blog (14.9 MB, v0.1.0, linux/amd64, revision 339228dbfeee) ``` `act build` prints the `go build` command it runs, so a CI runner or a container build without `act` runs the same command with Go alone. `CGO_ENABLED=0` makes the binary static, so it runs on any machine of the same operating system and architecture, even in a minimal container image. [Deployment](../deployment.md) covers building for another system with `--target`, and a `Dockerfile`. ## Generate a production key Production needs an `APP_KEY` of its own, never the one in your `.env`: anyone who has the key can forge the cookies and signed URLs it signs. With `APP_ENV=production`, `act generate:key` prints a new key instead of writing it to `.env`: ```sh APP_ENV=production act generate:key ``` It prints the key on a line of its own, `APP_KEY=` followed by 43 characters, then a reminder: ```text title="Output" APP_ENV is production, which reads no .env file: set APP_KEY in your platform's environment ``` ## Run in production `APP_ENV=production` puts the App in the production Mode. In production, the App reads no `.env` file at all, so every variable comes from the real environment: set them in your platform's settings or your service definition. Everything else in `.env.example` has a default: the App listens on port `3000` unless `PORT` says otherwise, and `APP_DEBUG` stays off, so a server error's cause never reaches a client. Run the binary with the key you generated: ```sh APP_ENV=production APP_KEY=your_app_key ./bin/blog ``` ```text title="Output" {"time":"2026-10-21T01:13:52.967764859+02:00","level":"INFO","msg":"server started","addr":"[::]:3000","took":823476} ``` Outside development, the App logs one JSON object per line, ready for a log collector. A variable the App needs and can't find stops it at boot, before it listens, with every missing variable listed together: ```sh APP_ENV=production ./bin/blog ``` ```text title="Output" env: APP_KEY is missing ``` The binary exits with status `1`, so your platform reports a failed deployment instead of an App that fails its first request. ## Probe the health checks Load balancers and orchestrators such as Kubernetes probe an App to decide whether to send it requests, and whether to restart it. The skeleton registers both probes in `start/routes.go`, with `app.Health`. The liveness probe checks only that the App answers: ```sh curl localhost:3000/health/live ``` ```text title="Output" {"status":"ok"} ``` The readiness probe runs the App's checks: here, one that the disk isn't full. It answers `200` while every check passes and `503` when one fails, so a load balancer stops sending requests to this instance without restarting it: ```sh curl localhost:3000/health/ready ``` ```text title="Output" {"healthy":true,"status":"ok","finished_at":"2026-10-21T01:13:53.994157515+02:00","checks":[{"name":"disk space /","status":"ok","cached":false,"duration_ns":215302}]} ``` When a database replaces the in-memory store, add a check for it in `start/routes.go`, so an instance that can't reach the database stops receiving requests. The routes answer as they did under `act serve`: ```sh curl localhost:3000/posts ``` ```text title="Output" [{"id":1,"title":"Hello, Tinka","body":"The first post on the blog."},{"id":2,"title":"Routes and handlers","body":"How a request finds its handler."}] ``` ## Run a Command from the binary `act` isn't installed on a server, and doesn't need to be: the binary runs its own Commands. Run `posts:list` with the same environment: ```sh APP_ENV=production APP_KEY=your_app_key ./bin/blog posts:list ``` ```text title="Output" 1 Hello, Tinka 2 Routes and handlers ``` ## Stop the App Platforms stop an App with `SIGTERM` when they replace it, and Ctrl+C sends `SIGINT`. On either, the App stops taking new connections, lets the requests in flight finish, runs its shutdown hooks, and exits: ```text title="Output" {"time":"2026-10-21T01:13:54.020346272+02:00","level":"INFO","msg":"server shutting down"} {"time":"2026-10-21T01:13:54.020535836+02:00","level":"INFO","msg":"server stopped"} ``` ## Recap `act build` built the tagged App into one static binary, `bin/blog`. It runs in production with `APP_ENV=production` and its `APP_KEY` from the environment, answers its health checks for load balancers, runs `posts:list` without `act`, and shuts down gracefully on `SIGTERM`. That completes the tutorial: the `blog` App lists, shows and creates posts, shares its store through a service provider, has a Command for people and agents, and tests for all of it. From here, the guides cover each part in depth. [Middleware](../guides/basics/middleware.md) adds behavior around the routes, [Logger](../guides/digging-deeper/logger.md) writes your own log records, and [Emitter](../guides/digging-deeper/emitter.md) lets one part of the App react when another creates a post. See also: [Deployment](../deployment.md), [Configuration & Environment](../configuration.md), [Health checks](../guides/digging-deeper/health-checks.md), [Application lifecycle](../guides/concepts/application-lifecycle.md) --- Source: https://tinka.tmuco.dev/contributing.md # Contributing > Work on Tinka itself: get the code, run the checks, build a package with its tests and docs, and commit on the version branch. This page is for people and agents working on Tinka itself, not on an App built with it. Tinka holds every change to the same standard: code, tests and a docs page ship together, and one Command checks all of it. The sections below follow a change from getting the code to committing it. :::note The Tinka repository is private during the alpha. You need a GitHub account with access to clone it. ::: ## Get the code Tinka needs Go 1.27 or later. Clone the repository; its default branch is `1.x`, the Version branch for Tinka 1, and there is no `main`: ```sh git clone git@github.com:mucolabs/tinka.git cd tinka ``` Install the Tinka CLI from your checkout. Inside the repository, it forwards the contributor Commands, such as `tinka verify`, to the repository’s own tooling: ```sh go install ./cmd/tinka ``` Agents start with `AGENTS.md` at the repository root: it maps every directory and links the files that govern them. `GLOSSARY.md` defines the project’s vocabulary, and every page, comment and commit uses its terms. ## The workspace layout The repository holds two Go modules, tied together for local work by `go.work`: - **The root module**, `github.com/mucolabs/tinka`: Core in the root package, the packages beside it (`cli`, `di`, `env`, `log`, `encryption`, `password`, `events`, `health`, `tinkatest`), and the `tinka` and `act` Commands under `cmd/`. This is what Apps depend on. - **The `dev` module**: the contributor Commands and the docs site renderer. Its dependencies, such as the linter and the Markdown renderer, live in its own `go.mod`, so they never reach an App. - **The starter kits** under `kits/`: `_skeleton` and `_core`, embedded in the Tinka CLI. Each keeps its `go.mod` as `go.mod.kit`, and its directory starts with an underscore so the go command doesn’t build it as part of Tinka. To try a change in a new App, point `tinka new` at your checkout with `--local`, so the App builds against your code instead of a release: ```sh tinka new blog --local ~/code/tinka ``` `package.json` and the `*.config.ts` files deploy the docs site. Nothing in Tinka depends on Node. ## Run the checks `go run ./dev verify` runs every check at once: `gofmt`, `go mod tidy` for both modules outside the workspace (the way `go install` builds them), `golangci-lint` (which includes `go vet`) and `go test -race`. The tests build the docs too, so a broken page fails verify. Test results stay cached, so unchanged packages don’t run again, and verify lowers its own priority so the machine stays responsive. With the Tinka CLI installed, `tinka verify` runs the same thing. ```sh go run ./dev verify ``` The CI workflow runs this one command and nothing else. A new check goes in `dev/verify.go`, never in the workflow file. Preview the docs with live reload at `localhost:3000`, or build the static site, into `dev/site/dist` unless you pass a directory: ```sh go run ./dev docs serve go run ./dev docs build ``` ## How a package is built Tinka grows one package at a time, and each one follows the same steps, recorded in `handbook/README.md` with the status of every package: ::::steps :::step{title="Research"} Study the problem and survey how the Go standard library, `golang.org/x` and trusted Go packages solve it. Write the notes to `handbook/research/`. ::: :::step{title="Agree on the approach"} Describe the approach and its trade-offs to the maintainer, and agree on it before writing code. ::: :::step{title="Build it with its tests"} Write the package with the standard library’s `testing` package only. Output is checked against golden files, refreshed with `-update`, such as `go test ./cli -update`. ::: :::step{title="Ship the docs with it"} The same change adds or updates its docs page, any new terms in `GLOSSARY.md`, and a decision record under `docs/adr/` when the decision is hard to reverse, surprising and a real trade-off. ::: :::: The docs describe Tinka on its own terms. They never compare it with another framework or runtime; each such comparison, and the reason behind it, goes in the package’s page under `handbook/`, which is never published. A change is done when all of it lands together and `go run ./dev verify` passes. ## Dependencies Every dependency becomes a dependency of every App, so Tinka adds none lightly. The root module uses the standard library first, then `golang.org/x`. A module from anywhere else needs the maintainer’s agreement before it’s added, with the evidence the `tinka-dependencies` skill asks for: one job, a stable v1 or later, wide use, active maintenance, few dependencies of its own, a measured cost in startup time and binary size, and an MIT, BSD or Apache 2.0 license. The rule covers test-only dependencies and the `dev` module too. ## Writing docs Load the `tinka-docs-writer` skill in `.agents/skills/` before planning, writing or reviewing a page under `docs/content`. It sets where a page goes, its template, the voice and the authoring syntax. Every page must be listed in `docs/content/nav.json`: the build fails on a page missing from it, bad frontmatter, an unknown `:::` block or fence option, or a link to a page that does not exist. Every color, size and motion value on the docs site, and on any other Tinka surface, comes from `DESIGN.md`. A value that isn’t there doesn’t ship until it’s added there first. ## Commits and branches Commit finished work on `1.x`. Messages follow Conventional Commits, with a scope naming the area and a lowercase summary of what the change does: ```text feat(act): act build builds the App for production fix(http): record 101 Switching Protocols only once a hijack succeeds feat(kits): the skeleton tests with tinkatest, in parallel ``` The types are `feat`, `fix`, `docs`, `refactor`, `test` and `chore`. Releases are tags on the Version branch: `v1.0.0-alpha.N`, then `-beta.N`, then `v1.0.0`. :::warning Every push to `1.x` deploys the docs site, so a push publishes whatever docs the branch holds. Run `go run ./dev verify` before you push, and push only when the change is ready to be read. ::: See also: [Philosophy](philosophy.md), [Installation](installation.md), [FAQs](faqs.md) --- Source: https://tinka.tmuco.dev/releases.md # Releases > Every Tinka release by month, with what it holds and its tag. Tinka is released from the `1.x` branch, and every release is a tag on it: `v1.0.0-alpha.N` during the alpha, then betas, then `v1.0.0`. `go install github.com/mucolabs/tinka/cmd/tinka@latest` installs the newest tag, and `tinka version` prints the one you have. Pin the version your App builds against in its `go.mod`, since APIs change between alpha releases. The [Roadmap](roadmap.md) lists what comes next. ## October 2026 `v1.0.0-alpha.1` is the first release: Tinka's foundation, complete with its docs. | Description | Tag | |---|---| | The Tinka CLI and `act`, with Agent mode for every Command | `v1.0.0-alpha.1` | | `tinka new` with the skeleton and core starter kits | `v1.0.0-alpha.1` | | `act serve`, `act build`, `act test` and the `act make` generators | `v1.0.0-alpha.1` | | Routing, handlers that return values and errors, middleware and problem details | `v1.0.0-alpha.1` | | The body parser, file uploads, and signed, encrypted and plain cookies | `v1.0.0-alpha.1` | | The App lifecycle, dependency injection and service providers | `v1.0.0-alpha.1` | | Typed configuration from the environment and `.env` files | `v1.0.0-alpha.1` | | The logger, the emitter and health checks | `v1.0.0-alpha.1` | | Password hashing with Argon2id, and encryption with the App's key | `v1.0.0-alpha.1` | | Commands with arguments, flags, prompts and output components | `v1.0.0-alpha.1` | | HTTP tests with `tinkatest`, and console tests with `clitest` | `v1.0.0-alpha.1` | See also: [Roadmap](roadmap.md), [Installation](installation.md), [Introduction](introduction.md) --- Source: https://tinka.tmuco.dev/roadmap.md # Roadmap > The first-party packages planned after Tinka's foundation, and what Tinka leaves out on purpose. Tinka grows one package at a time, and each one ships with its tests and its docs page, or it does not ship. The foundation is done: [the Introduction](introduction.md#status-and-stability) lists what an App can use today, and [Releases](releases.md) records what each version holds. This page lists what comes next. The roadmap has no dates. A package is ready when it is complete, tested and documented, and its API settles before `v1.0.0`. ## Planned packages ### Validation Typed validation of request bodies, query strings and route params, with every failing field reported together in one problem details response. The [body parser](guides/basics/body-parser.md) already decodes bodies into typed values and accepts a validator, so validation builds on what handlers do today. ### Sessions and authentication Sessions that keep state between requests, logging users in and out, guards that protect routes, and authorization checks that decide what a signed-in user may do. Passwords already hash with [Argon2id](guides/security/hashing.md), and cookies are [signed and encrypted](guides/basics/response.md#set-cookies) with the App's key. ### Data layer Access to SQL databases, migrations that evolve the schema with the App, and models for reading and writing rows. Until it ships, open a database with `database/sql` and register the pool in a [service provider](guides/concepts/service-providers.md), as the [FAQs](faqs.md#how-do-i-use-a-database-today) show. ### HTTP security A static file server with caching headers, CORS for APIs that browsers call from other origins, CSRF protection for forms, and rate limiting. Today `app.Handle` serves files with the standard library's `http.FileServerFS`, as [Routing](guides/basics/routing.md#serve-other-handlers) shows, and `net/http` middleware already runs in Tinka's chain through `tinka.Std`, so you can add a package you trust in the meantime: see [Middleware](guides/basics/middleware.md#use-nethttp-middleware). ## Left out on purpose Some things Tinka does not plan to add, because the language or the principles already answer them: - **Controllers.** A handler is a function, and handlers that share a dependency are methods on a struct. There is no controller type to learn. - **A configuration loader.** Configuration is typed Go code that reads the environment, as [Configuration & Environment](configuration.md) shows, so the compiler checks it. - **A REPL.** Write a [Command](guides/command-line/commands.md) for a task you run by hand, and it ships with the App. See also: [Releases](releases.md), [Philosophy](philosophy.md), [Contributing](contributing.md) --- Source: https://tinka.tmuco.dev/guides/basics/routing.md # Routing > Map URLs and HTTP methods to handlers that return values, with path parameters, groups, names and middleware. This guide covers routing in Tinka. You will learn how to: - Register routes whose handlers return the response - Read path parameters - Group routes under a prefix - Name routes and build their URLs - Add middleware to a group or to one route - Serve any `http.Handler` next to your routes - Catch route mistakes when the App starts ## Overview Routes connect a method and a URL to a handler. Tinka registers them on the standard library's `http.ServeMux`, adding groups, names and per-route middleware, so every pattern you know from `net/http` works unchanged and a request costs no more than it would with `ServeMux` alone. A handler receives the request's [HTTP context](http-context.md), a `*tinka.Ctx`, and returns a value and an error. The value becomes the response and the error becomes an error response, so a handler reads top to bottom and never writes the same response twice. ## Register a route Create an App with `tinka.New`, register handlers on it, and serve it. `Serve` listens on `:3000` and shuts down gracefully when the process is stopped, as [Application lifecycle](../concepts/application-lifecycle.md) explains. ```go title="main.go" package main import ( "context" "log/slog" "os" "github.com/mucolabs/tinka" ) func main() { app := tinka.New(tinka.HTTPConfig{}) app.Get("/posts", listPosts) app.Get("/posts/{id}", showPost) if err := app.Serve(context.Background()); err != nil { slog.Error("server failed", "err", err) os.Exit(1) } } ``` An App is also an `http.Handler`, so tests and other servers can run it directly. The handlers return the posts they found. A struct, a slice or a map is sent as JSON, and an error becomes the response described in [Exception handling](exception-handling.md): ```go title="posts.go" package main import ( "github.com/mucolabs/tinka" ) type Post struct { ID int64 `json:"id"` Title string `json:"title"` } func listPosts(c *tinka.Ctx) ([]Post, error) { return []Post{{ID: 1, Title: "Hello, Tinka"}}, nil } func showPost(c *tinka.Ctx) (Post, error) { id, err := c.ParamInt("id") if err != nil { return Post{}, err } if id != 1 { return Post{}, tinka.NotFound("No post %d.", id) } return Post{ID: id, Title: "Hello, Tinka"}, nil } ``` `Get`, `Post`, `Put`, `Patch` and `Delete` register one method each. A `Get` route also answers `HEAD`, and a request with a method the path doesn't support gets a `405 Method Not Allowed` listing the allowed methods in its `Allow` header. The [Response](response.md) guide lists every kind of value a handler can return, such as HTML pages, redirects and files. ## Read path parameters A `{name}` segment matches one path segment, and `{name...}` matches the rest of the path. `c.Param` returns the value, and `c.ParamInt` returns it as a whole number, answering `422` for anything else. ```go title="files.go" package main import ( "github.com/mucolabs/tinka" ) func showFile(c *tinka.Ctx) (string, error) { return "file: " + c.Param("path"), nil } ``` Registered as `app.Get("/files/{path...}", showFile)`, a request for `/files/docs/intro.md` reads `docs/intro.md`. Values arrive decoded, and an escaped slash (`%2F`) stays inside its segment. When two patterns match a request, the more specific one wins, whatever order they were registered in: `/posts/new` wins over `/posts/{id}` for the request `/posts/new`. ## Group routes under a prefix `Route` gives a set of routes a shared prefix, which may contain parameters. Inside it, `"/"` is the prefix itself, exactly. ```go title="main.go" ins={4-8} app := tinka.New(tinka.HTTPConfig{}) app.Get("/posts", listPosts) app.Get("/posts/{id}", showPost) app.Route("/teams/{team}", func(r *tinka.Router) { r.Get("/", showTeam) r.Get("/members", listMembers) r.Delete("/members/{id}", removeMember) }) ``` This registers `GET /teams/{team}`, `GET /teams/{team}/members` and `DELETE /teams/{team}/members/{id}`. A `GET` for `/teams/go/` answers `308` to `/teams/go`, so every resource has one canonical URL and links with a trailing slash still work. ## Name routes `As` names a route, so links to it are built from the name instead of repeating its path. `URL` fills the route's parameters in order: ```go title="main.go" ins={2,3} app := tinka.New(tinka.HTTPConfig{}) app.Get("/posts", listPosts).As("posts.index") app.Get("/posts/{id}", showPost).As("posts.show") ``` `app.URL("posts.show", 7)` returns `/posts/7`. Changing the route's path then updates every link to it. The [URL builder](url-builder.md) guide covers query strings, escaping and wildcards. ## Add middleware `Use` adds [middleware](middleware.md) to the routes registered after it, inside the router you call it on. `Group` scopes middleware without adding a prefix, and `With` adds it to one route: ```go title="main.go" app.Route("/admin", func(r *tinka.Router) { r.Use(requireAdmin) r.Delete("/posts/{id}", deletePost) }) app.Group(func(r *tinka.Router) { r.Use(requireSignIn) r.Post("/posts", createPost) }) app.With(tinka.Timeout(5*time.Second)).Get("/reports/{id}", showReport) ``` `app.Use` is different: it adds middleware to every request the App handles, matched or not, as the [Middleware](middleware.md) guide explains. :::warning A router's `Use` must come before the routes it applies to. Calling it after a route on the same router, or on one made from it, panics at startup, because those routes would silently skip the middleware. ```go title="main.go" del={4} ins={2} app.Group(func(r *tinka.Router) { r.Use(requireSignIn) r.Post("/posts", createPost) r.Use(requireSignIn) }) ``` Move `Use` above the routes, or use `With` for a single route. ::: ## Serve other handlers `Handle` registers any `http.Handler`, such as a file server, for one method. An empty method matches every method, and a `{name...}` wildcard matches every path below a prefix. The handler writes its own response, after the route's middleware ran. ```go title="main.go" files := http.FileServerFS(assets) app.Handle("GET", "/assets/{file...}", http.StripPrefix("/assets/", files)) ``` ## Catch mistakes at startup A duplicate route, two patterns that conflict (such as `/posts/{id}` and `/posts/{slug}`), a pattern that doesn't start with `/`, or two routes with the same name makes registration panic. The App fails when it starts, never on the first request that would have hit the mistake. See also: [Application lifecycle](../concepts/application-lifecycle.md), [HTTP context](http-context.md), [Middleware](middleware.md), [Response](response.md), [URL builder](url-builder.md), [App](../../reference/app.md) --- Source: https://tinka.tmuco.dev/guides/basics/http-context.md # HTTP context > The *tinka.Ctx every handler and middleware receives: the request, the response, the request's ID and its dependencies. This guide covers the HTTP context in Tinka. You will learn how to: - Use the `*tinka.Ctx` a handler receives - Pass it wherever a `context.Context` is expected - Reach the request and the response writer when you need them - Resolve request-scoped dependencies from the container ## Overview Every request gets one **HTTP context**, a `*tinka.Ctx`, which the App passes to each middleware and to the handler. It holds everything about the request in one place: the incoming `*http.Request`, the response being built, the request's ID, and the request's dependencies. Handlers take it as their only argument, so their signatures stay the same however much of the request they use: ```go title="posts.go" package main import ( "github.com/mucolabs/tinka" ) type Post struct { ID int64 `json:"id"` Title string `json:"title"` } func showPost(c *tinka.Ctx) (Post, error) { id, err := c.ParamInt("id") if err != nil { return Post{}, err } return Post{ID: id, Title: "Hello, Tinka"}, nil } ``` A Ctx has methods for the [request](request.md), such as `Param`, `Query`, `IP` and `Accepts`, and for the [response](response.md), such as `Status` and `Header`. Those guides cover them. ## Pass it as a context A Ctx is a `context.Context`: its deadline, cancellation and values are the request's. Pass it to the database, to HTTP clients and to your own code, and their work stops when the client disconnects or the request times out: ```go title="posts.go" func (s *PostService) Find(ctx context.Context, id int64) (Post, error) { var p Post err := s.db.QueryRowContext(ctx, "SELECT id, title FROM posts WHERE id = $1", id).Scan(&p.ID, &p.Title) return p, err } func (h Posts) Show(c *tinka.Ctx) (Post, error) { id, err := c.ParamInt("id") if err != nil { return Post{}, err } return h.Service.Find(c, id) } ``` Records you log with it carry the request's ID as `request_id`, as the [Logger](../digging-deeper/logger.md) guide shows. `c.ID()` returns the same ID, which the response also carries in its `X-Request-Id` header. ## Reach the request and the response writer `c.Request()` returns the `*http.Request`, for anything the Ctx has no method for, such as request headers: ```go agent := c.Request().Header.Get("User-Agent") ``` `c.ResponseWriter()` returns the `http.ResponseWriter`, for a handler that writes the response itself, such as one that streams Server-Sent Events. Once anything is written, the handler's returned value is ignored. When a middleware replaces the request or the writer, as `tinka.Timeout` and compression middleware do, both methods return the replacement. ## Resolve request-scoped dependencies Give the App your [dependency injection](../concepts/dependency-injection.md) container, and `c.Make` resolves from it through a resolver made for this request. A `Scoped` binding is built once per request, and can ask for the `*tinka.Ctx` itself: ```go title="main.go" container := di.New() container.Scoped(func(r *di.Resolver) (*Cart, error) { c := r.MustMake[*tinka.Ctx]() return carts.ForSession(c, c.Query("session")) }) app := tinka.New(tinka.HTTPConfig{Container: container}) app.Get("/cart", func(c *tinka.Ctx) (*Cart, error) { return c.Make[*Cart]() }) ``` Resolve singletons at boot instead, and capture them in your handlers: a request that never calls `Make` pays nothing for the container. :::warning A Ctx belongs to one request. Keeping it after the handler returns, for example in a goroutine that outlives the request, reads a request that's already finished, whose context is canceled. ```go title="posts.go" del={1} ins={2-3} go notifyFollowers(c, post) ctx := context.WithoutCancel(c.Request().Context()) go notifyFollowers(ctx, post) ``` Pass the background work what it needs, and a context detached from the request's with `context.WithoutCancel`: it keeps the request's values, such as its ID for the logs, without the Ctx or its cancellation. ::: See also: [Request](request.md), [Response](response.md), [Middleware](middleware.md), [Dependency injection](../concepts/dependency-injection.md) --- Source: https://tinka.tmuco.dev/guides/basics/middleware.md # Middleware > Run code around handlers: for every request, for a group or for one route, with errors as values and net/http middleware through tinka.Std. This guide covers middleware in Tinka. You will learn how to: - Write middleware that runs before and after the handler - Apply it to every request, to a group or to one route - End a request from middleware - See and handle the errors handlers return - Use middleware written for `net/http` ## Overview **Middleware** is code that runs around handlers: it checks a request before the handler sees it, or adjusts the response after. Authentication, CORS, rate limits and timing headers are middleware, because they apply to many routes and have nothing to do with any one handler. A middleware is a function that receives the [HTTP context](http-context.md) and `next`, which runs the rest of the chain and returns its error. The handler's response is written only after every middleware has returned, so code after `next` can still set headers and the status. ## Write middleware This middleware measures how long the rest of the request takes, and reports it in a `Server-Timing` header the browser's developer tools display: ```go title="timing.go" package main import ( "fmt" "time" "github.com/mucolabs/tinka" ) func timing(c *tinka.Ctx, next tinka.Next) error { start := time.Now() err := next(c) c.Header().Set("Server-Timing", fmt.Sprintf("app;dur=%d", time.Since(start).Milliseconds())) return err } ``` Code before `next` runs on the way in, and code after it on the way out. Return the error `next` returned, so the App can turn it into a response. ## Apply middleware `app.Use` adds middleware that runs for every request, before routing, in the order added. Requests that match no route pass through it too, so it suits CORS, security headers and request logging: ```go title="main.go" app := tinka.New(tinka.HTTPConfig{}) app.Use(timing) app.Get("/posts", listPosts) ``` A router's `Use`, `Group` and `With` add middleware to some routes only, as the [Routing](routing.md) guide shows. For a request to `/admin/posts`, middleware runs from the outside in: the App's, then the group's, then the route's, then the handler. Upstream, after `next`, the order reverses. ```go title="main.go" app.Use(timing) // every request app.Route("/admin", func(r *tinka.Router) { r.Use(requireAdmin) // every route in /admin r.With(audited).Delete("/posts/{id}", deletePost) // this route }) ``` Middleware that takes options is a function that returns one: ```go title="auth.go" package main import ( "github.com/mucolabs/tinka" ) func requireRole(role string) tinka.Middleware { return func(c *tinka.Ctx, next tinka.Next) error { user, err := c.Make[*User]() if err != nil { return err } if user.Role != role { return tinka.Forbidden("Only %ss can do this.", role) } return next(c) } } ``` Registered with `r.Use(requireRole("editor"))`, it reads the signed-in user, then refuses the request or lets it through. ## End a request Return an error without calling `next`, and nothing after the middleware runs: the error becomes the response, as `requireRole` above does with `tinka.Forbidden`. Middleware that ran before it still finish their code after `next`, so the timing header is set even on a refused request. ## Handle errors from handlers `next` returns the handler's error, or the error of any middleware after yours. Inspect it with `errors.Is` and `errors.As`, wrap it, or replace it. Return it to let the App render it: ```go title="metrics.go" package main import ( "errors" "github.com/mucolabs/tinka" ) func countFailures(c *tinka.Ctx, next tinka.Next) error { err := next(c) if e, ok := errors.AsType[*tinka.Error](err); ok && e.Status >= 500 { serverErrors.Add(1) } return err } ``` A panic in a handler is not an error: it unwinds through the middleware, skipping their code after `next`, and the App recovers it and answers `500`. ## Use net/http middleware `tinka.Std` adapts middleware written for `net/http`, with the `func(http.Handler) http.Handler` shape, such as a CORS or compression package: ```go title="main.go" app.Use(tinka.Std(cors.Default().Handler)) // github.com/rs/cors ``` The request and the `ResponseWriter` it passes on reach the handler, so a compression middleware compresses the response. Unlike Tinka middleware, `net/http` middleware expects the response to be written when its `next` returns, so the response is written there: middleware outside it still sees the handler's error, but can no longer change the response. ## Time out slow requests `tinka.Timeout` cancels the request's context after a duration. A handler that passes its context to slow calls then returns early, and the client gets a `503`: ```go title="main.go" app.With(tinka.Timeout(5*time.Second)).Get("/reports/{id}", showReport) ``` See also: [Routing](routing.md), [HTTP context](http-context.md), [Exception handling](exception-handling.md) --- Source: https://tinka.tmuco.dev/guides/basics/request.md # Request > Read path parameters, the query string, headers, the client's IP behind proxies, and the content type the client prefers. This guide covers reading the request in Tinka. You will learn how to: - Read path parameters and query string values - Read request headers and cookies - Find the client's IP address, also behind a proxy - Answer with the format the client prefers ## Overview The [HTTP context](http-context.md) has methods for the parts of a request handlers read most. Each returns a plain Go value, and anything they don't cover is on the `*http.Request` that `c.Request()` returns. The body has its own guide, [Body parser](body-parser.md), because it needs decoding and validation. ## Path parameters `c.Param` returns a value the route pattern matched, and `c.ParamInt` returns it as a whole number, answering `422` when it isn't one: ```go title="comments.go" package main import ( "github.com/mucolabs/tinka" ) type Comment struct { PostID int64 `json:"post_id"` Slug string `json:"slug"` } // Registered as app.Get("/posts/{id}/comments/{slug}", showComment). func showComment(c *tinka.Ctx) (Comment, error) { postID, err := c.ParamInt("id") if err != nil { return Comment{}, err } return Comment{PostID: postID, Slug: c.Param("slug")}, nil } ``` ## Query string `c.Query` returns the first value of a query string parameter, or `""` when the request has none. `c.QueryValues` returns them all as `url.Values`, for parameters that repeat. The query string is parsed once per request, however often you read it. ```go title="posts.go" // GET /posts?page=2&tag=go&tag=web func listPosts(c *tinka.Ctx) ([]Post, error) { page := c.Query("page") // "2" tags := c.QueryValues()["tag"] // ["go", "web"] return posts.List(c, page, tags) } ``` Query values are text the client chose. Parse and check them before use, as you would a form field: `strconv.Atoi(c.Query("page"))` returns an error for `?page=two`. ## Headers Read request headers from the request. `Header.Get` is case-insensitive and returns `""` for a missing header: ```go agent := c.Request().Header.Get("User-Agent") languages := c.Request().Header.Values("Accept-Language") ``` `c.Header()` returns the response's headers instead, which the [Response](response.md) guide covers. ## Cookies `c.Cookie` reads a cookie set with [`c.SetCookie`](response.md#set-cookies) as the type you name, and reports whether the request has it with a valid signature. A cookie the client changed, or a value copied from another cookie, is reported missing, so a handler can trust what it gets: ```go title="cart.go" func showCart(c *tinka.Ctx) (Cart, error) { cart, ok := c.Cookie[Cart]("cart") if !ok { return Cart{}, nil // no cart yet, or one that was tampered with } return cart, nil } ``` `c.EncryptedCookie` reads a cookie set with `c.SetEncryptedCookie`, the same way. `c.PlainCookie` reads a plain cookie, or any cookie the browser sent, as a string: treat it as user input, like the query string. ## Client IP address `c.IP` returns the client's IP address. Without a proxy, it's the address of the connection, and a client can't change it with a header. Behind a load balancer or reverse proxy, every connection comes from the proxy, which adds the client's address to the `X-Forwarded-For` header. List your proxies in `TrustedProxies`, and `c.IP` reads the header, from the nearest hop back, until it reaches an address no trusted proxy added: ```go title="main.go" app := tinka.New(tinka.HTTPConfig{ TrustedProxies: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")}, }) ``` Without `TrustedProxies`, only a proxy on the same machine (a loopback address) is trusted. An empty list trusts none. :::warning Trust only the proxies in front of your App. Trusting a range clients can reach directly, such as every address with `0.0.0.0/0`, lets any client pick the IP your App sees by sending its own `X-Forwarded-For`, which defeats rate limits and audit logs keyed by IP. ```go title="main.go" del={2} ins={3} app := tinka.New(tinka.HTTPConfig{ TrustedProxies: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")}, TrustedProxies: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/8")}, }) ``` ::: ## Content negotiation `c.Accepts` returns the format the client prefers among the ones you offer, by the request's `Accept` header and its quality values. Offer media types, such as `application/json`, or file extensions, such as `json` and `html`. With no `Accept` header the first offer wins, and so does the earlier offer when the client likes two equally. It returns `""` when the client accepts none of them. ```go title="posts.go" func showPost(c *tinka.Ctx) (tinka.Response, error) { post, err := posts.Find(c, c.Param("id")) if err != nil { return nil, err } if c.Accepts("json", "html") == "html" { return tinka.HTML(renderPost(post)), nil } return tinka.JSON(post), nil } ``` The App negotiates the same way for errors: a browser that asks for HTML gets an error page, and every other client gets problem details. See also: [HTTP context](http-context.md), [Body parser](body-parser.md), [Response](response.md) --- Source: https://tinka.tmuco.dev/guides/basics/response.md # Response > Return values that become responses: JSON, text, HTML, redirects, files and streams, with the status and headers you choose. This guide covers building responses in Tinka. You will learn how to: - Return values that become JSON, text or HTML responses - Set the status code and response headers - Redirect, also back to the previous page - Set signed, encrypted and plain cookies - Send files, downloads and streams - Write the response yourself when you need to - Create your own response types ## Overview A handler returns its response as a value. The App writes it once every middleware has returned, so middleware can still set headers and the status, and a handler can never write two responses. A value is encoded as soon as the handler returns, so a value that can't be encoded becomes an ordinary error, never half a response. The type a handler returns decides how its value is sent: | Returned value | Sent as | Status | |---|---|---| | A struct, slice, map or number | `application/json` | `200` | | A `string` | `text/plain; charset=utf-8` | `200` | | `tinka.HTML` | `text/html; charset=utf-8` | `200` | | `[]byte` | `application/octet-stream` | `200` | | `nil` | no body | `204` | | A `tinka.Response`, such as `tinka.Redirect` | what the Response writes | its own | JSON keeps a struct's field order, sorts map keys, and encodes nil slices and maps as `[]` and `{}`, so clients never receive `null` for an empty list. ## Return JSON Return the value. Handlers that always answer the same type declare it, so the compiler checks every return: ```go title="posts.go" package main import ( "github.com/mucolabs/tinka" ) type Post struct { ID int64 `json:"id"` Title string `json:"title"` Tags []string `json:"tags"` } func showPost(c *tinka.Ctx) (Post, error) { return Post{ID: 1, Title: "Hello, Tinka"}, nil } ``` ```json title="Response" {"id":1,"title":"Hello, Tinka","tags":[]} ``` ## Set the status and headers `c.Status` sets the status code, and `c.Header` returns the response's headers. Both can change until the response is written: ```go title="posts.go" func createPost(c *tinka.Ctx) (Post, error) { post := Post{ID: 2, Title: "Second post"} c.Status(http.StatusCreated) c.Header().Set("Location", "/posts/2") return post, nil } ``` A status set with `c.Status` wins over the default for the returned value, so a handler can answer `202 Accepted` with no body by setting the status and returning `nil`. ## Return text and HTML A `string` is sent as plain text. A page is `tinka.HTML`, sent as `text/html`: ```go title="pages.go" package main import ( "bytes" "html/template" "github.com/mucolabs/tinka" ) var postPage = template.Must(template.New("post").Parse(`

{{.Title}}

`)) func showPostPage(c *tinka.Ctx) (tinka.HTML, error) { var page bytes.Buffer if err := postPage.Execute(&page, Post{Title: "Hello, "}); err != nil { return "", err } return tinka.HTML(page.String()), nil } ``` :::warning Build HTML with `html/template`, which escapes what it inserts. Text a user wrote, put into a `tinka.HTML` as it is, runs as markup in every visitor's browser: a comment containing `