<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: yramstech</title>
    <description>The latest articles on Forem by yramstech (@yramstech).</description>
    <link>https://forem.com/yramstech</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3893103%2F3ba09138-1a4e-4518-a3b9-717064690a80.png</url>
      <title>Forem: yramstech</title>
      <link>https://forem.com/yramstech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/yramstech"/>
    <language>en</language>
    <item>
      <title>How I Built an Android Keyboard in Kotlin for Two Ghanaian Languages</title>
      <dc:creator>yramstech</dc:creator>
      <pubDate>Fri, 24 Apr 2026 11:51:40 +0000</pubDate>
      <link>https://forem.com/yramstech/how-i-built-an-android-keyboard-in-kotlin-for-two-ghanaian-languages-5deg</link>
      <guid>https://forem.com/yramstech/how-i-built-an-android-keyboard-in-kotlin-for-two-ghanaian-languages-5deg</guid>
      <description>&lt;p&gt;In late 2022 I got tired of watching friends type Ewe and Twi messages with English keyboards, missing all the letters that make those languages what they are. The diacritics — ɛ, ɔ, ŋ, ɖ, ƒ, ʋ, ɣ — were either auto-corrected out of existence or typed with workarounds like "3" for ɛ or ")","or" and "o" for ɔ.&lt;/p&gt;

&lt;p&gt;So I gave myself two months to ship an Android Input Method Editor (IME) that would let Ewe and Twi speakers type natively, with every diacritic one tap away.&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;February 25, 2023&lt;/strong&gt;, &lt;a href="https://play.google.com/store/apps/details?id=com.bless.ewetwikeyboard" rel="noopener noreferrer"&gt;Ewe Twi Keyboard&lt;/a&gt; went live on the Google Play Store. Three years and &lt;strong&gt;10,000+ installs&lt;/strong&gt; later, it's on v4 with a premium tier and a steady install rate.&lt;/p&gt;

&lt;p&gt;Here's what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem — deeper than I thought
&lt;/h2&gt;

&lt;p&gt;Before writing the first line of code, I tried every existing "African language keyboard" on Play Store. The results fell into three buckets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;English keyboards with an emoji-style diacritic panel.&lt;/strong&gt; Users had to tap twice for every accented character. Nobody would use that for a sentence, let alone a conversation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keyboards that treated Ewe and Twi as afterthoughts.&lt;/strong&gt; The interesting insight here is linguistic: every Twi letter is also an Ewe letter. Ewe's alphabet is a superset. So one well-designed keyboard can serve both languages natively — but most of what I tried bolted the extra letters onto an English layout without thinking about how people actually type mixed-language messages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Abandoned apps.&lt;/strong&gt; Beautiful ideas from 2016–2019, no updates since, broken on Android 10+.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The design goal became clear: &lt;strong&gt;one keyboard, both languages, no language-switching dance, every diacritic accessible in ≤ 1 extra tap.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;The v4 app today is &lt;strong&gt;Kotlin end-to-end&lt;/strong&gt;, with a clean separation between the keyboard service (which Android treats specially as an IME) and the rest of the app. It wasn't always this way — the 2023 release was Java — but more on that below. Here's the current stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kotlin + InputMethodService&lt;/strong&gt; — every Android IME extends this system service class. The OS decides when to show/hide the keyboard; I just provide the View.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jetpack Compose + Material 3&lt;/strong&gt; — used for the settings screen, onboarding, and premium-upgrade flow. The actual keyboard layout stayed on the classic Android View system for performance reasons (you cannot afford Compose recomposition latency on every keystroke).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean architecture&lt;/strong&gt; — &lt;code&gt;service/&lt;/code&gt;, &lt;code&gt;domain/&lt;/code&gt;, &lt;code&gt;data/&lt;/code&gt;, &lt;code&gt;ui/&lt;/code&gt;. A &lt;code&gt;KeyboardLayoutProvider&lt;/code&gt; caches 200+ key objects so I'm not rebuilding them on every keystroke; a &lt;code&gt;WordPredictor&lt;/code&gt; handles suggestions; an &lt;code&gt;EmojiProvider&lt;/code&gt; serves the emoji panel; a &lt;code&gt;LocaleHelper&lt;/code&gt; toggles English, French, and Spanish for the settings UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DataStore Preferences&lt;/strong&gt; — for user settings (theme, sound, vibration, auto-capitalise). Async-first, avoids the pitfalls of SharedPreferences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coroutines&lt;/strong&gt; throughout — for billing checks, update prompts, and anything that could block the IME thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase Crashlytics + Analytics&lt;/strong&gt; — an IME can crash the whole user experience across every app, so telemetry is non-optional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Play Billing (BillingClient)&lt;/strong&gt; — a premium tier removes ads. A coupon validator lets beta users and early supporters unlock it for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Play In-App Updates&lt;/strong&gt; — if a user is on an old version, the app nudges them to update inline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Mobile Ads SDK (AdMob)&lt;/strong&gt; — banner ads in the settings screen only (never in the keyboard view itself — that would violate Play policy and user trust).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lottie Compose&lt;/strong&gt; — animated onboarding illustrations that show users how to enable the keyboard from Android settings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No backend. No accounts. No text logging. Zero network calls from the keyboard view itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on 2023 vs 2026: the Java → Kotlin rewrite
&lt;/h2&gt;

&lt;p&gt;The stack above describes the app as it exists today. The honest 2023 version was different: I originally shipped Ewe Twi Keyboard in &lt;strong&gt;Java&lt;/strong&gt;. It worked, it passed Play Store review in 24 hours, and it had everything the first release needed.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;January and February 2026&lt;/strong&gt; I migrated the whole codebase to Kotlin, file by file, with Java and Kotlin coexisting in the repo during the transition. The trigger was Jetpack Compose: the new settings screens, onboarding flow, and premium-upgrade UI all wanted Compose — which is Kotlin-only. Rather than bolt Compose onto a Java app with a thin Kotlin surface, I committed to the full migration.&lt;/p&gt;

&lt;p&gt;Deliberate ordering: &lt;strong&gt;the UI layer migrated first, the IME service last&lt;/strong&gt;. Compose forced the settings, theme, and premium screens into Kotlin early. The &lt;code&gt;CustomKeyboardService&lt;/code&gt; — the thing that actually runs inside every other app while you type — I saved for the end. A bug in an IME doesn't just break your own app; it makes every other app feel broken because users suddenly can't type. I wanted the surrounding data, domain, and UI layers stable and in idiomatic Kotlin before touching the keyboard surface itself.&lt;/p&gt;

&lt;p&gt;Android Studio's "Convert Java File to Kotlin" action was a starting point, not an endpoint. Every converted file needed a second pass for idiomatic Kotlin — nullability, extension functions, coroutines where callbacks used to live. But the interop layer Google ships is better than its reputation: the app stayed buildable and shippable throughout the migration, with mixed-language modules compiling cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diacritic design
&lt;/h2&gt;

&lt;p&gt;The core interaction pattern: long-press a base letter, a popup appears with every diacritic variant, tap the one you want.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tap &lt;code&gt;e&lt;/code&gt; → you get &lt;code&gt;e&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Long-press &lt;code&gt;e&lt;/code&gt; → popup shows &lt;code&gt;e&lt;/code&gt;, &lt;code&gt;ɛ&lt;/code&gt;, and the tone marks &lt;code&gt;ɛ́&lt;/code&gt;, &lt;code&gt;ɛ̀&lt;/code&gt;, &lt;code&gt;ɛ̃&lt;/code&gt;, &lt;code&gt;ɛ̂&lt;/code&gt;, &lt;code&gt;ɛ̌&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Same pattern for &lt;code&gt;o&lt;/code&gt; (→ &lt;code&gt;ɔ&lt;/code&gt; and its tonal variants), &lt;code&gt;n&lt;/code&gt; (→ &lt;code&gt;ŋ&lt;/code&gt;), &lt;code&gt;d&lt;/code&gt; (→ &lt;code&gt;ɖ&lt;/code&gt;), &lt;code&gt;f&lt;/code&gt; (→ &lt;code&gt;ƒ&lt;/code&gt;), &lt;code&gt;v&lt;/code&gt; (→ &lt;code&gt;ʋ&lt;/code&gt;), &lt;code&gt;g&lt;/code&gt; (→ &lt;code&gt;ɣ&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A fluent user never leaves the home row. Every diacritic is one long-press away — not a separate layer, not a separate keyboard, not an emoji panel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing to Play Store — the paperwork, not the code
&lt;/h2&gt;

&lt;p&gt;Keyboards get extra scrutiny from Google. Because IMEs see every keystroke a user types — passwords, credit card numbers, private messages — Play Console requires explicit answers to Data Safety questions about what the app stores, logs, or transmits.&lt;/p&gt;

&lt;p&gt;My answers were simple because the architecture was simple: &lt;strong&gt;nothing leaves the device.&lt;/strong&gt; No keystroke logging, no typed-text analytics, no cloud sync of any kind. The review passed in 24 hours.&lt;/p&gt;

&lt;p&gt;The manifest still needs the standard IME plumbing — a service declaration with the &lt;code&gt;BIND_INPUT_METHOD&lt;/code&gt; permission and the &lt;code&gt;android.view.InputMethod&lt;/code&gt; intent filter, plus a &lt;code&gt;method.xml&lt;/code&gt; that declares the subtypes Android shows in the system keyboard picker. Standard Android IME boilerplate; nothing exotic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that surprised me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Views beat Compose for the keyboard itself.&lt;/strong&gt; I tried building the key grid in Compose early on. Recomposition latency on every keystroke was visible even on a Pixel. I fell back to a classic &lt;code&gt;View&lt;/code&gt; subclass for the keyboard surface and haven't regretted it. Compose is fantastic — just not inside the hot path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Localisation is a first-class design constraint, not a feature.&lt;/strong&gt; Once I accepted every keypress had to be optimal for mixed-language use, design decisions got easier. The "one keyboard, both languages" insight flowed naturally from watching how my friends actually type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crashlytics pays for itself in week one.&lt;/strong&gt; An IME crash doesn't just break your app — it makes every other app feel broken because users can't type. Telemetry caught three OEM-specific bugs I never saw on my own device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The narrowest-market projects have the most loyal users.&lt;/strong&gt; Ewe Twi Keyboard will never compete with Gboard. But the users who install it &lt;em&gt;use&lt;/em&gt; it every day. Three years after launch, the app has &lt;strong&gt;10,000+ installs on Google Play&lt;/strong&gt;, is on v4 with a premium tier, and keeps growing — proof that shipping small into a specific community compounds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On Google Play (10,000+ installs):&lt;/strong&gt; &lt;a href="https://play.google.com/store/apps/details?id=com.bless.ewetwikeyboard" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.bless.ewetwikeyboard&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tech:&lt;/strong&gt; Kotlin · InputMethodService · Jetpack Compose · DataStore · Firebase · Google Play Billing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're a Kotlin developer wondering what to build for your community — look at the little frictions your friends complain about. The audience is narrower than "everyone," but it's real, and they notice when you ship something that works.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>android</category>
      <category>mobile</category>
      <category>localization</category>
    </item>
    <item>
      <title>I Built an AI Text Summarizer in One Night with Claude + Next.js</title>
      <dc:creator>yramstech</dc:creator>
      <pubDate>Wed, 22 Apr 2026 22:26:59 +0000</pubDate>
      <link>https://forem.com/yramstech/i-built-an-ai-text-summarizer-in-one-night-with-claude-nextjs-3p9</link>
      <guid>https://forem.com/yramstech/i-built-an-ai-text-summarizer-in-one-night-with-claude-nextjs-3p9</guid>
      <description>&lt;p&gt;It was a Monday. I had the Anthropic API key provisioned, Next.js scaffolded, and one self-imposed deadline: ship a working demo live on the public internet before midnight.&lt;/p&gt;

&lt;p&gt;A few hours later, &lt;strong&gt;&lt;a href="https://ai-summarizer-next.vercel.app/" rel="noopener noreferrer"&gt;https://ai-summarizer-next.vercel.app/&lt;/a&gt;&lt;/strong&gt; went live. You paste any block of text, pick a style — concise, bullet points, executive summary, or ELI5 — and Claude Sonnet 4.6 gives you back a clean summary in about a second.&lt;/p&gt;

&lt;p&gt;Here's how I did it, what the code looks like, and a few things that surprised me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Every day, we skim more words than we read. Articles, meeting notes, policy documents, contract clauses, research papers — the wall of text never stops. Existing summarization tools are either buried inside other products (Notion AI, Gmail) or too generic (just "tl;dr" with no control over tone or length).&lt;/p&gt;

&lt;p&gt;I wanted something trivially fast, with a few quality knobs, and completely free for the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 14&lt;/strong&gt; (App Router) — one framework for UI + API in one repo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; — catches the silly mistakes I always make at 10 PM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; — styling without leaving the TSX file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic SDK&lt;/strong&gt; — Claude Sonnet 4.6 as the brain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; — one &lt;code&gt;git push&lt;/code&gt; and the world has access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total cost: &lt;strong&gt;$0&lt;/strong&gt;. Anthropic gives generous free credits for new accounts, Vercel's hobby tier is free, and everything else is open-source.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt (simpler than you'd think)
&lt;/h2&gt;

&lt;p&gt;The entire "AI" logic is one parameterised system prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a summarization expert. Summarize in a ${style} style.
Return ONLY the summary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. I spent more time tuning the UI than the prompt. Claude is smart enough that you don't need to bludgeon it with instructions — you need to be &lt;em&gt;specific about what you want&lt;/em&gt; and &lt;em&gt;quiet about everything else&lt;/em&gt;. The line &lt;code&gt;Return ONLY the summary&lt;/code&gt; is what keeps the model from prefacing every response with "Here's a summary:".&lt;/p&gt;

&lt;h2&gt;
  
  
  The API route
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;app/api/summarize/route.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@anthropic-ai/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`You are a summarization expert. Summarize in a &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;concise&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; style. Return ONLY the summary.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thirty lines. One Claude call. That's the whole backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UI
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;app/page.tsx&lt;/code&gt; is a textarea, a dropdown, and a button. A feature doesn't have to look complicated to be useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setText&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStyle&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;concise&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSummary&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/summarize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;setSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"max-w-2xl mx-auto p-8"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-3xl font-bold mb-2"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;AI Summarizer&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-600 mb-4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Powered by Claude Sonnet 4.6&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;textarea&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"w-full p-3 border rounded-lg mb-3"&lt;/span&gt; &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Paste text to summarize..."&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex gap-2 mb-4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;select&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"border rounded-lg p-2"&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;option&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"concise"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Concise&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;option&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;option&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bullet points"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Bullet Points&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;option&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;option&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"executive summary"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Executive Summary&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;option&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;option&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"ELI5"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;ELI5&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;option&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;select&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;summarize&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bg-black text-white px-6 py-2 rounded-lg disabled:opacity-50"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarizing...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Summarize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bg-gray-100 p-4 rounded-lg whitespace-pre-wrap"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploying to Vercel
&lt;/h2&gt;

&lt;p&gt;Three shell commands and one web click:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git init &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git add &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"AI Summarizer on Next.js"&lt;/span&gt;
git remote add origin https://github.com/YOUR_USERNAME/ai-summarizer-next.git
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then on &lt;a href="https://vercel.com/new" rel="noopener noreferrer"&gt;vercel.com/new&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Import the repo&lt;/li&gt;
&lt;li&gt;Add environment variable: &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; = your key&lt;/li&gt;
&lt;li&gt;Click Deploy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;About 60 seconds later the URL is live.&lt;/p&gt;

&lt;p&gt;The single most important thing to get right: &lt;strong&gt;the environment variable must be set BEFORE you click Deploy.&lt;/strong&gt; If you forget, the build succeeds but every request hits a 500. Add the env var first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that surprised me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How boring the code got.&lt;/strong&gt; I expected LLM-powered apps to need careful error handling, streaming, complicated state — but Claude is quick enough on a 1k-token summary that a plain request/response flow feels instant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How clean the Anthropic SDK is.&lt;/strong&gt; &lt;code&gt;new Anthropic()&lt;/code&gt; just works if the env var is named &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;. No config object, no initialisation dance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How much the UI matters.&lt;/strong&gt; I spent more time on textarea padding and the button's disabled state than on the API integration. That felt wrong, but users judge trust from design, not from clever prompts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it + see the code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://ai-summarizer-next.vercel.app/" rel="noopener noreferrer"&gt;https://ai-summarizer-next.vercel.app/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; [&lt;a href="https://github.com/yramstech/ai-summarizer-next/" rel="noopener noreferrer"&gt;https://github.com/yramstech/ai-summarizer-next/&lt;/a&gt;]&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've been putting off your first Claude API project — it's genuinely a Tuesday-night, one-sitting build. Pick a narrow problem, resist the urge to add features, and hit deploy before midnight.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nextjs</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
