Calling a language model API
Send a real prompt to a hosted model (Anthropic's Claude API) and get structured JSON back — the heart of DayOne's AI feature.
This is the feature that makes DayOne more than a text box: the user taps Reflect, and the app sends their entry to a language model and gets back a title, a mood, and a reflective question. We'll use Anthropic's Claude API (check Anthropic's model list for current options), but the pattern is identical for any hosted model.
Ask the model for structured output
The trick to using an LLM inside an app is to demand a strict output shape so you can decode it. We ask for JSON with exactly the fields our Entry needs:
struct Insights: Decodable {
let title: String
let mood: String // one word, e.g. "reflective"
let reflection: String // a follow-up question
}The request
The call is the same async throws + URLSession shape from Module 4, with an HTTP POST, headers, and a JSON body describing the messages. Note the system instruction that forces JSON-only output:
import Foundation
enum InsightsService {
static func generate(for entryText: String, apiBase: URL) async throws -> Insights {
// We call OUR backend (apiBase), which holds the secret key. See next lesson.
var request = URLRequest(url: apiBase.appendingPathComponent("reflect"))
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(["entry": entryText])
let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(Insights.self, from: data)
}
}On the backend (a tiny serverless function you'll set up next lesson), the actual Claude call looks like this. The system prompt is what guarantees clean JSON:
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic(); // reads ANTHROPIC_API_KEY from env
export default async function handler(req, res) {
const { entry } = req.body;
const message = await anthropic.messages.create({
model: "claude-opus-4-8",
max_tokens: 300,
system:
"You analyze a journal entry. Reply with ONLY minified JSON of the form " +
'{"title": string, "mood": one word, "reflection": a short question}. No prose.',
messages: [{ role: "user", content: entry }],
});
// Claude returns text; it's our JSON string.
res.status(200).json(JSON.parse(message.content[0].text));
}Get your API key
- 1Create an account at console.anthropic.com.
- 2Add a payment method under Billing and buy a small amount of credit — $5 is plenty for this course.
- 3Go to API Keys ▸ Create Key. Copy the key once and store it somewhere safe — you can't view it again later.
- 4Never paste the key into your app's code. The next lesson shows where it actually goes.
An app needs predictable data, not a paragraph. By instructing the model to return only JSON matching your Decodable struct, you turn an open-ended model into a reliable function. This 'structured output' pattern is the backbone of nearly every LLM feature in production apps.
Each reflection is a few hundred tokens in and out — about a cent per call with a flagship model, and a fraction of that with a smaller model (plenty for structured extraction like this). For a journaling app used a few times a day, monthly cost is trivial. Still, you'll add guardrails (a cap on entry length, and a Reflect button that's disabled while a call is in flight) so a bug can't run up a bill.