Practical recipes.

Examples you can download and run, and prompts you can hand to your agent, for the things most web apps end up needing.

Build a website with templates.

Pages are HTML files in views/. template() fills them with data from your route: {{value}} prints escaped text, {{#for}} and {{#if}} handle lists and conditions, and {{> header}} pulls in a shared partial from views/partials/. This example has a home page that lists posts, a page for each post, and a 404 page.

views/home.html
{{> header}}
<main>
<h1>Posts</h1>
<ul>
{{#for post in posts}}
  <li><a href="/posts/{{post.slug}}">{{post.title}}</a>: {{post.summary}}</li>
{{#empty}}
  <li>Nothing here yet.</li>
{{/for}}
</ul>
</main>
</body>
</html>

Output is escaped unless you use triple braces, so a title like <b> shows up as text instead of markup. The intent file checks the listing, the escaping, and the 404.

server.tnt · server.intent · views/home.html · views/post.html · views/missing.html · views/partials/header.html

bash
# Keep the same layout: views/ and views/partials/ beside server.tnt.
ntnt intent check server.tnt -vv
ntnt run server.tnt

Validate form input.

std/validate composes schemas and validators, returning cleaned data or field-level errors. This example accepts a name and email, returns 201 for valid input, and 422 for an invalid email. It does not create an account or send mail.

Download server.tnt · Download server.intent

bash
# Save both files in the same directory.
ntnt intent lint server.intent
ntnt intent check server.tnt -vv
ntnt run server.tnt

It’s the same code shown on the homepage. When you change it, rerun the check so both the valid and rejected cases still pass.

Add Google and GitHub login.

std/auth has built-in settings for Google, GitHub, Microsoft, Apple, Discord, and other OAuth providers, plus OpenID Connect discovery for the rest. enable_auth registers the login, callback, and logout routes, stores sessions, and serves a basic sign-in page at /auth.

server.tnt
import { html, redirect } from "std/http/server"
import { oauth, enable_auth, get_user } from "std/auth"
import { get_env } from "std/env"

let google = oauth("google", get_env("GOOGLE_CLIENT_ID") ?? "", get_env("GOOGLE_CLIENT_SECRET") ?? "")
let github = oauth("github", get_env("GITHUB_CLIENT_ID") ?? "", get_env("GITHUB_CLIENT_SECRET") ?? "")
enable_auth([google, github], map { "session_secret": get_env("SESSION_SECRET") ?? "" })

fn dashboard(req) {
    let user = get_user(req) otherwise { return redirect("/auth") }
    return html("<h1>Hello, </h1>")
}

get("/dashboard", dashboard)
listen(8080)

Register /auth/google/callback and /auth/github/callback as redirect URLs with each provider. Keep the client secrets and a long random SESSION_SECRET in your environment, and use a SQLite, PostgreSQL, or Redis session store in production so sign-ins survive a restart.

Authentication API reference →

Sign in with a one-time email link.

std/auth handles one-time magic-link tokens and sessions. Your application decides who can sign in, sends the email, and controls which routes the user can access.

Give your agent this next step:

Add passwordless sign-in using std/auth's magic_link_flow. Use an explicit SITE_URL and a durable token store. Keep eligibility and authorization in application callbacks. Deliver through std/email with NTNT_EMAIL_MODE=log for development; do not send real messages during tests. Test ineligible requests, expired links, replay rejection, and access to a protected route.

Authentication API reference →

Send email without holding up the request.

std/email has SMTP, batch delivery, and a log transport for local work. std/jobs adds scheduling, retries, dead letters, durable claims, and execution leases. Put slow delivery in a job rather than keeping a browser request open.

Add a welcome-email job using std/jobs and std/email. Keep development on NTNT_EMAIL_MODE=log. Persist a delivery identity so retries and unknown outcomes can be reconciled. Show pending, completed, failed, and outcome_unknown separately. Test a successful log-transport delivery and a forced failure; do not call a live SMTP service from verification.

For Redis-backed jobs on 0.5.5, use Redis 7.0+ or Valkey 7.2+ and upgrade every writer and worker sharing the queue together. A crash after an external send can leave an uncertain outcome; a retry is not automatically safe.

Run network checks.

NTNT has native network operations, persistent ICMP probes, and bounded SNMPv2c monitoring. Since 0.5.5, a check can carry a start deadline, and std/http.probe_fetch brings the same deadline to HTTP. A late check is rejected before it reaches the network.

Build a small NTNT network-check page using native networking APIs. Keep ICMP ping distinct from TCP reachability. Limit targets, concurrency, and time budgets; preserve actual sample times. Show target failure separately from execution failure or missed start deadline. Test with local fixtures, and require explicit authorization for any target beyond the local test setup.

See Net Bacon in the showcase → · Read the deadline contract

Intent tests without an HTTP server.

A glossary term with call: and source: calls an ordinary NTNT function directly, with no server running. The slug example checks the conversion, whitespace trimming, and that the same input always gives the same result.

Download slug.tnt · Download slug.intent

bash
# Save both files in the same directory.
ntnt intent lint slug.intent
ntnt intent check slug.intent -vv

Function tests suit business rules and data transformations. Cover the expected values and the edge cases, and rerun them whenever the function changes.

Keep credentials out of the output.

std/secrets looks secrets up by name and returns values that print as redacted. Outbound calls that support secrets can use them, but logs, templates, and ordinary serialization can’t reveal them. Where the secrets come from in production is up to your deployment.

Read the secrets and magic-link capability boundaries →