tinka alpha

Routing

This guide covers routing in Tinka. You will learn how to:

Overview

Routes connect a method and a URL to a handler. Tinka registers them on the standard library's http.ServeMux, adding groups, prefixes 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.

Register a route

Create an App with tinka.New, register handlers on it, and serve it. An App is an http.Handler, so any net/http server runs it.

main.go
package main

import (
	"log"
	"net/http"
	"time"

	"github.com/mucolabs/tinka"
)

func main() {
	app := tinka.New(tinka.HTTPConfig{})
	app.Get("/posts", listPosts)
	app.Get("/posts/{id}", showPost)

	srv := &http.Server{
		Addr:              ":3000",
		Handler:           app,
		ReadHeaderTimeout: 5 * time.Second,
	}
	log.Fatal(srv.ListenAndServe())
}

A handler takes the response writer and the request, and returns an error. Returning the error is enough: the App turns it into the response, as Return errors from handlers explains.

posts.go
package main

import (
	"net/http"

	"github.com/mucolabs/tinka"
)

type post struct {
	ID    int64  `json:"id"`
	Title string `json:"title"`
}

func listPosts(w http.ResponseWriter, r *http.Request) error {
	return tinka.JSON(w, http.StatusOK, []post{{ID: 1, Title: "Hello, Tinka"}})
}

func showPost(w http.ResponseWriter, r *http.Request) error {
	id, err := tinka.PathInt(r, "id")
	if err != nil {
		return err
	}
	return tinka.JSON(w, http.StatusOK, post{ID: id, Title: "Hello, Tinka"})
}

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.

Read path parameters

A {name} segment matches one path segment, and {name...} matches the rest of the path. Read them with r.PathValue, or with tinka.PathInt when the value must be a whole number, which answers 422 for anything else.

files.go
package main

import (
	"io"
	"net/http"
)

func showFile(w http.ResponseWriter, r *http.Request) error {
	_, err := io.WriteString(w, "file: "+r.PathValue("path"))
	return err
}

Registered as app.Get("/files/{path...}", showFile), a request for /files/docs/intro.md reads docs/intro.md.

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.

main.go
	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.

Add middleware

Middleware has the standard func(http.Handler) http.Handler shape, so existing net/http middleware works as is. Use applies middleware to the routes registered after it, inside the router you call it on:

main.go
	app.Route("/admin", func(r *tinka.Router) {
		r.Use(requireToken(adminToken))
		r.Delete("/posts/{id}", deletePost)
	})

Group scopes middleware without adding a prefix, and With adds it to one route:

main.go
	app.Group(func(r *tinka.Router) {
		r.Use(requireToken(adminToken))
		r.Post("/posts", createPost)
	})
	slow := app.With(tinka.Timeout(5 * time.Second))
	slow.Get("/reports/{id}", showReport)

Middleware that can refuse a request returns a tinka.HandlerFunc, so it reports failures the same way handlers do:

auth.go
package main

import (
	"net/http"

	"github.com/mucolabs/tinka"
)

func requireToken(token string) tinka.Middleware {
	return func(next http.Handler) http.Handler {
		check := func(w http.ResponseWriter, r *http.Request) error {
			if r.Header.Get("Authorization") != "Bearer "+token {
				return tinka.Unauthorized("A valid token is required.")
			}
			next.ServeHTTP(w, r)
			return nil
		}
		return tinka.HandlerFunc(check)
	}
}

tinka.Timeout cancels the request's context after the given time. A handler that passes its context to slow calls then returns early, and the client gets a 503.

Serve other handlers

Handle registers any http.Handler, such as a file server, for one method. An empty method matches every method, and a pattern ending in / matches every path below it.

main.go
	files := http.FileServerFS(assets)
	app.Handle("GET", "/assets/", http.StripPrefix("/assets/", files))

Catch mistakes at startup

A duplicate route, two patterns that conflict (such as /posts/{id} and /posts/{slug}), or a pattern that doesn't start with / makes registration panic. The App fails when it starts, never on the first request that would have hit the mistake.

See also: Body parser, App