# Recipe: An app in front of an existing API > A Next.js-style web framework for Go: a folder under app/ is a route, HTML is written in Go, and nothing outside the standard library is imported. > /trilha/llms/recipes/existing-api.txt - [An app in front of an existing API](/trilha/cookbook/existing-api): Forwarding /api/ to a service that already exists, with a login of your own. · 3147 tokens est. - [Upstreams](/trilha/reference/upstreams): Forwarding a URL prefix to an API that already exists, with the session's credential. · 1244 tokens est. - [auth](/trilha/reference/auth): Provider, Options, Auth, User and Store — the API of the auth package, with the defaults and what each field changes. · 10541 tokens est. 3 page(s) · 14932 tokens est. in all --- The shape of most migrations: the users are yours — e-mail, password hash, role — and the API stays where it is. Two pieces of Trilha meet here, and they meet because the proxy has to carry what the login stored. `examples/local-login` is this recipe, whole and running. ## 1. The session, without a provider `auth.Sessions` is the same `*Auth` the OIDC flow uses, without a provider. `Require()`, `RequireRole()`, the `Store`, the rotation of the identifier and the idle window are the code that was already there; what changes is who says the person is who they claim to be. ```go var Flow = auth.Sessions(auth.Options{ Store: auth.NewMemoryStore(), Idle: 30 * time.Minute, LoginPath: "/entrar", AfterLogin: "/painel", }) ``` `User.Extra` is what this session carries and OIDC has no claim for — the token the API expects, the tenant, the plan. It travels where the rest of the session travels, so keep it small and never put a password in it: ```go func Entrar(c *trilha.Ctx, u usuarios.Usuario) error { return Flow.Login(c, &auth.User{ Subject: u.ID, Email: u.Email, Name: u.Nome, Roles: []string{u.Papel}, // The token of the API the upstream forwards to. It travels in the // session, never in the page. Extra: map[string]string{"api_token": u.Token}, }) } ``` ## 2. The password, checked against your own table `auth.CheckPBKDF2` reads the format Django and `hashlib` write — `pbkdf2_sha256$iterations$salt$hash` — so the users table that already exists stays valid, hashes and all. `auth.HashPBKDF2` writes a new one at 600,000 iterations. Answer the same thing for a wrong e-mail and for a wrong password, and take the same time: saying which of the two was wrong tells an attacker who has an account. ```go func (s *Store) Verify(email, senha string) (Usuario, error) { s.mu.RLock() u, ok := s.rows[strings.ToLower(strings.TrimSpace(email))] s.mu.RUnlock() if !ok { auth.CheckPBKDF2(semUsuario, senha) return Usuario{}, ErrCredencial } if !auth.CheckPBKDF2(u.Hash, senha) { return Usuario{}, ErrCredencial } return u, nil } ``` The handler is a form like any other: ```go func POST(c *trilha.Ctx) error { email, senha := c.Form("email"), c.Form("senha") u, err := trilha.Use[*usuarios.Store](c).Verify(email, senha) if err != nil { return c.Render(422, formulario(c, email, "E-mail ou senha inválidos.")) } return sessao.Entrar(c, u) } ``` ## 3. The API, on the same origin ```go func Config(cfg *trilha.Config) { // Where the trail of who did what goes. Without this it would go to the // log, which is enough to grep and enough to ship with. cfg.Audit = sessao.Auditoria // O app é escrito para quem lê em português, e é isto que decide o idioma // dos componentes do kit que trazem texto próprio — a tela de auditoria // entre eles — e o fuso em que as datas dela aparecem. cfg.Locale = "pt-BR" cfg.TimeZone = "America/Sao_Paulo" if api := os.Getenv("API_URL"); api != "" { cfg.Upstreams = map[string]trilha.Upstream{ "/api/": { Target: api, Headers: func(c *trilha.Ctx, hdr http.Header) { if tok := sessao.Token(c); tok != "" { hdr.Set("Authorization", "Bearer "+tok) } }, }, } } } ``` The proxy answers outside the middleware chain, so nobody put the user in the request context: read the session from the cookie. ```go func Token(c *trilha.Ctx) string { u, err := Flow.Session(c) if err != nil { return "" } return u.Extra["api_token"] } ``` ## The types the API already publishes The proxy above is for the browser. The page is not the browser: it runs on the server, and what it needs is not a URL, it is the shape of the answer — which the API already publishes, in its own OpenAPI document. ```bash trilha client http://api:8801/openapi.json --out internal/api ``` That writes one file, committed like `trilha_gen.go`, with a struct per schema and a method per operation. `trilha client ... --check` in CI fails the day the document moves and nobody regenerated. The generated client imports the standard library and nothing else — not even Trilha — so it works in a job and in a test too. ```go // Acervo is the client of the API the front used to call from the browser. It // is generated by `trilha client` from the document the API publishes, so the // compiler is what catches the field that changed name. var Acervo = api.New("http://api:8801", api.WithHeader(sessionToken), api.WithResponse(collectCookies)) ``` The credential travels in the context, not in the client: ```go // tokenKey is how the credential travels from the request to the client. The // client takes a context, not a *trilha.Ctx: it also runs in a job and in a // test, where there is no request to borrow a token from. type tokenKey struct{} // Calling is the context to hand the client: the session's token, and nothing // else about the request. func Calling(c *trilha.Ctx) context.Context { tok, _ := c.Signed("api_token") return context.WithValue(c.Context(), tokenKey{}, tok) } // sessionToken is the only place the credential appears. It runs for every // request the client makes, so a call from a context that carries no token // goes out unauthenticated instead of borrowing somebody else's session. func sessionToken(ctx context.Context) http.Header { tok, _ := ctx.Value(tokenKey{}).(string) if tok == "" { return nil } return http.Header{"Authorization": {"Bearer " + tok}} } ``` When the API is authenticated by **cookie**, the credential goes the other way: it comes back in a `Set-Cookie`, and the operation that produces it is the login. `WithResponse` runs for every response the client receives and hands over its status and its headers, so the collector is the same idea as the token — it lives in the context of the call, and belongs to this request: ```go // cookieKey is the other direction of tokenKey. On an API authenticated by // cookie the credential comes back in a Set-Cookie, and the operation that // produces it is the login — so the page has to read the response and not only // the body. The collector travels in the context of the call, which is what makes // it right on a server: a http.CookieJar on the client would belong to the // process, and would send one person's credential on the next person's call. type cookieKey struct{} // Collecting is Calling for an operation whose answer is also in the header: the // context to hand the client, and where the cookies it set will be once the call // returns. func Collecting(c *trilha.Ctx) (context.Context, *[]*http.Cookie) { var set []*http.Cookie return context.WithValue(Calling(c), cookieKey{}, &set), &set } // collectCookies runs for every response the client receives, and fills the // collector of that call — nothing at all when the call did not bring one. func collectCookies(ctx context.Context, resp *http.Response) { if set, ok := ctx.Value(cookieKey{}).(*[]*http.Cookie); ok { *set = resp.Cookies() } } // SignIn hands the browser the credential the API set. The operation is whichever // one the API calls a login — here, the one this document happens to declare — // and what matters is that its Set-Cookie reaches the response of this request // and of no other. func SignIn(c *trilha.Ctx, in api.User) error { ctx, cookies := Collecting(c) if _, err := Acervo.Users().UpsertUser(ctx, in); err != nil { return err } for _, ck := range *cookies { ck.Domain = "" // the cookie is this app's host now, not the API's c.SetCookie(ck) } return c.Redirect("/documents") } ``` The same hook reads the `ETag` or the `Location` of a `POST` that creates and the `Link` of a paginated answer. What it does not offer is the body: the body belongs to the operation, which returns it decoded. And then the page is a page. There is no loading state, because the data is already in hand when the HTML is written: ```go // Documents is the page. It runs on the server, so there is no loading state // and no second endpoint: the data it renders is already in hand. func Documents(c *trilha.Ctx) (h.Node, error) { var q DocsQuery if err := c.Bind(&q); err != nil { return nil, err } page, err := Acervo.Documents().ListDocuments(Calling(c), api.DocumentsListDocumentsParams{ Page: int64(q.Page), PageSize: int64(q.PerPage), Q: q.Q, Status: api.Status(q.Status), }) if err != nil { return nil, err } q.ListParams.Restrict("filename", "status") return ui.DataTable(c, DocsColumns, page.Items, ui.ListState{ Params: q.ListParams, Total: int(page.Total), ID: "docs", Search: "search", }), nil } ``` The columns are typed by the API's own struct, so a field that stops existing stops compiling — which is the whole point of generating the client instead of reading JSON into a map: ```go // DocsColumns names the columns of the table. The types are the API's, so a // field that stops existing stops compiling. var DocsColumns = ui.Columns[api.Document]{ {Key: "filename", Label: "File", Sort: true}, {Key: "status", Label: "Status", Cell: func(d api.Document) h.Node { return ui.Badge(h.Text(string(d.Status))) }}, {Key: "pages", Label: "Pages", Num: true, Cell: func(d api.Document) h.Node { return h.Text(strconv.Itoa(int(d.Pages))) }}, } ``` A binary answer arrives as the `*http.Response`, so the body streams straight through: ```go // DownloadDocument hands the API's answer straight to the browser: the body is // never read into memory, and the Content-Type the API chose is the one that // arrives. func DownloadDocument(c *trilha.Ctx) error { resp, err := Acervo.Documents().Download(Calling(c), c.Param("id")) if err != nil { return err } defer resp.Body.Close() return c.Inline(c.Param("id")+".pdf", resp.Body, resp.Header.Get("Content-Type")) } ``` The two doors are the same API and the same token: the page uses the client, the islands and any `fetch` keep calling `/api/*` on this origin. ```go // ProxyAPI is the other half. The page uses the client above; the islands and // any fetch still call /api/* on the same origin, and the upstream puts the // same credential on the request. One API, one token, two doors. func ProxyAPI(cfg *trilha.Config) { cfg.Upstreams = map[string]trilha.Upstream{ "/api/": { Target: "http://api:8801", Headers: func(c *trilha.Ctx, hdr http.Header) { if tok, _ := c.Signed("api_token"); tok != "" { hdr.Set("Authorization", "Bearer "+tok) } }, Timeout: 60 * time.Second, }, } } ``` A refused write comes back as an `*Error`, and when the API is a FastAPI the 422 it writes is already one message per field. `Fields` is that list, keyed by the name the form uses, so the page that failed re-renders itself with each message beside its input: ```go // CreateDocument is the write. A 422 is not an error page: it is the same form // again, with the API's own messages next to the fields that earned them. func CreateDocument(c *trilha.Ctx) (h.Node, error) { var in api.DocumentIn if err := c.Bind(&in); err != nil { return nil, err } doc, err := Acervo.Documents().CreateDocument(Calling(c), in) if e, ok := api.AsError(err); ok && e.Status == http.StatusUnprocessableEntity { return documentForm(in, trilha.FieldErrors(e.Fields)), nil } if err != nil { return nil, err } return nil, c.Redirect("/documents/" + doc.ID) } ``` `trilha.FieldErrors` is a `map[string]string` and so is `Fields`, so the conversion is the whole adapter: no second validation of the same rules on this side, and no version of them that drifts from the API's. What the generator will not pretend to understand — a `oneOf`, a schema with no type — comes through as `json.RawMessage` and is printed as a line of the report when it runs; an API with no `response_model` anywhere gets the count of how much of it stayed untyped. See [`trilha client`](/trilha/reference/cli#trilha-client). ## What you get for free - `POST /api/*` requires the CSRF token, like every other write of the app. - The browser's own `Authorization` never reaches the API — only the session's. - A 200 MB upload and a PDF coming back both stream, past `MaxBodyBytes` and the write deadline. - A `route.go` you write at `/api/documents` answers before the proxy, so the API moves to Go one endpoint at a time and the front never notices. `trilha audit` will tell you if the target is plain `http://` to another host, if the upstream injects nothing in an app that has a login, or if the login has no rate limit. See [Upstreams](/trilha/reference/upstreams) and [Auth](/trilha/reference/auth) for the details.