Response
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
- 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:
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
}{"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:
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:
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:
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:
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:
return tinka.Back("/posts"), nilSend 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:
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:
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:
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:
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