tinka alpha

Response

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

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:

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
}
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:

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:

pages.go
package main

import (
	"bytes"
	"html/template"

	"github.com/mucolabs/tinka"
)

var postPage = template.Must(template.New("post").Parse(`<h1>{{.Title}}</h1>`))

func showPostPage(c *tinka.Ctx) (tinka.HTML, error) {
	var page bytes.Buffer
	if err := postPage.Execute(&page, Post{Title: "Hello, <Tinka>"}); err != nil {
		return "", err
	}
	return tinka.HTML(page.String()), nil
}

Return different kinds of responses

A handler that answers in more than one form, such as a page for browsers and JSON for API clients, returns tinka.Response. tinka.JSON and tinka.HTML are both Responses:

posts.go
func showPost(c *tinka.Ctx) (tinka.Response, error) {
	post := Post{ID: 1, Title: "Hello, Tinka"}
	if c.Accepts("json", "html") == "html" {
		return tinka.HTML("<h1>Hello, Tinka</h1>"), nil
	}
	return tinka.JSON(post), nil
}

Redirect

tinka.Redirect sends the client to another URL, with 302 Found unless c.Status set another code. Build the URL of a named route with URL:

posts.go
func (h Posts) Create(c *tinka.Ctx) (tinka.Response, error) {
	post, err := h.Service.Create(c, "Second post")
	if err != nil {
		return nil, err
	}
	c.Status(http.StatusSeeOther)
	return tinka.Redirect(h.App.URL("posts.show", post.ID)), nil
}

tinka.Back sends the client back to the page it came from, by the Referer header, as a form handler does after a validation error. A referrer on another site, or none, sends it to the fallback you pass instead, so a link from elsewhere can't turn your App into an open redirect:

go
	return tinka.Back("/posts"), nil

Send files and streams

tinka.SendFile sends a file, with its Content-Type from its extension. It answers range requests, so videos seek and downloads resume, and If-Modified-Since requests with 304. A missing file is a 404. tinka.Download sends a file as an attachment the browser saves under the name you give:

invoices.go
func downloadInvoice(c *tinka.Ctx) (tinka.Response, error) {
	id, err := c.ParamInt("id")
	if err != nil {
		return nil, err
	}
	path := filepath.Join("storage/invoices", strconv.FormatInt(id, 10)+".pdf")
	return tinka.Download(path, fmt.Sprintf("Invoice %d.pdf", id)), nil
}

tinka.Stream sends what a reader reads, without holding it in memory, such as a large export or a body from another service. The reader is closed afterwards:

exports.go
func exportPosts(c *tinka.Ctx) (tinka.Response, error) {
	csv, err := exports.Posts(c)
	if err != nil {
		return nil, err
	}
	c.Header().Set("Content-Disposition", `attachment; filename="posts.csv"`)
	return tinka.Stream("text/csv", csv), nil
}

Write the response yourself

c.ResponseWriter() returns the http.ResponseWriter, for a response no value describes, such as Server-Sent Events. Once anything is written, the handler's returned value is ignored, so return nil:

events.go
func streamEvents(c *tinka.Ctx) (tinka.Response, error) {
	w := c.ResponseWriter()
	w.Header().Set("Content-Type", "text/event-stream")
	rc := http.NewResponseController(w)
	for event := range events.Subscribe(c) {
		if _, err := fmt.Fprintf(w, "data: %s\n\n", event); err != nil {
			return nil, err
		}
		if err := rc.Flush(); err != nil {
			return nil, err
		}
	}
	return nil, nil
}

Create a response type

A tinka.Response is any type with a Respond(c *tinka.Ctx) error method. The App calls it once every middleware has returned. It writes through c.ResponseWriter(), and uses the status set with c.Status when there is one:

csv.go
package main

import (
	"encoding/csv"
	"net/http"

	"github.com/mucolabs/tinka"
)

// CSV is a table sent as text/csv.
type CSV [][]string

func (rows CSV) Respond(c *tinka.Ctx) error {
	w := c.ResponseWriter()
	w.Header().Set("Content-Type", "text/csv; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	return csv.NewWriter(w).WriteAll(rows)
}

A handler returns it like any other value: func listPosts(c *tinka.Ctx) (CSV, error). An error type with a Respond method renders itself the same way when a handler returns it, as the Exception handling guide shows.

See also: HTTP context, Request, URL builder, Exception handling