← Back to list

I Built a Serverless “Smart Sentiment & Content Analyser” on AWS -Zero cost.

You know that feeling when you’re sitting in your AI class, learning about natural language processing, and everything seems so……

Sachinthaheshan · 2025-09-29 16:31 · 0 claps · 7.8 min read
#aws #sentiment-analysis #aws-s3 #amazon-comprehend
Open on Medium ↗
Wiki topics: EDU · Education & Learning ☁️ · DevOps & Cloud

I Built a Serverless “Smart Sentiment & Content Analyser” on AWS -Zero cost.

Frontend of the webapp

Frontend of the webapp

You know that feeling when you’re sitting in your AI class, learning about natural language processing, and everything seems so… theoretical? The professor is talking about sentiment analysis, named entity recognition, and all these fancy terms that sound like they belong in expensive research labs.Well, I was that student until last weekend when I decided to bridge the gap between classroom theory and real-world application. And the best part? I did it without the one thing students have least of money.

I stood up a tiny web app that analyzes any text for sentiment, key phrases, and named entities using Amazon Comprehend, AWS Lambda (Python), API Gateway, and a static site on Amazon S3. It’s fully serverless, free-tier friendly, and a great hands-on intro to AWS AI services.

What I Actually Built

Let me break down what this “Smart Sentiment & Content Analyser” actually does in student-friendly terms:

Imagine you could give this app any piece of text — like your frustrated tweet about finals, a product review you’re writing, or even your essay draft and it would tell you:

  • The Vibe: Is this text happy, sad, angry, or mixed? 😊 😐 😠
  • Main Points: What are the key topics being discussed?
  • Who/What/Where: Can it spot names, companies, places?

So if I input: “I love studying at Stanford University, but the coffee at their cafeteria is terrible.”

The app would output:

  • Vibe: Mixed feelings (because there’s both love and hate)
  • Main Points: “studying”, “Stanford University”, “coffee”, “cafeteria”
  • Entities: “Stanford University” (it’s an organization)

See? Suddenly those boring NLP concepts from class make actual sense!

Why This Project Doesn’t Suck for Beginners

I’ve tried following tutorials before that promise “beginner-friendly” projects but then assume you have years of cloud experience. This one’s different because.

Amazon S3 — The Digital Backpack

  • What it is: Simple Storage Service (fancy name for cloud file storage).
  • Student analogy: Think of it like Google Drive, but programmable.
  • Why we use it: To store our website files (HTML, CSS, JS) because it’s dead simple and crazy reliable.

AWS Lambda — The Magical Helper

  • What it is: Serverless compute service (translation: code that runs only when needed).
  • Student analogy: Like having a friend who only shows up when you text them, does exactly what you ask, then disappears.
  • Why we use it: No servers to manage = no headaches = perfect for students.

API Gateway — The Receptionist

  • What it is: Manages API endpoints (fancy term for web addresses that accept requests).
  • Student analogy: Like a building receptionist who directs visitors to the right rooms.
  • Why we use it: It safely connects our website to our Lambda function.

Amazon Comprehend — The Brain

  • What it is: AI service for understanding text.
  • Student analogy: That friend who’s amazing at reading between the lines.
  • Why we use it: Pre-trained AI means we don’t need to build ML models from scratch.

System AWS Architecture

System AWS Architecture

Moment #1: When I Realised Serverless Isn’t Just Buzzword Bingo

In class, we learn about servers, virtual machines, and all that infrastructure stuff. But with Lambda, I wrote my Python code, uploaded it, and it just worked. No “ssh-ing” into machines, no installing dependencies, no worrying about scaling.

It hit me: this is why cloud computing is a game-changer. We can focus on writing code that solves problems, not on managing infrastructure.

Moment #2: When Three Lines of Code Did What Used to Take Semesters

Here’s the Python code that blew my mind:

# This is literally all it takes to analyze text sentiment
sentiment = comprehend.detect_sentiment(Text=my_text)

# And these two lines get key phrases and entities
phrases = comprehend.detect_key_phrases(Text=my_text)
entities = comprehend.detect_entities(Text=my_text)

In my NLP course, we spent weeks learning how to build sentiment analysis models. Don’t get me wrong — understanding the theory is important. But seeing how accessible these capabilities have become was genuinely exciting.

Moment #3: When I Understood How Everything Connects

This was the biggest lightbulb moment. Each AWS service is like a specialized team member:

  • S3 is the front desk: “Here’s our website, welcome!”
  • API Gateway is the coordinator: “You have a request? Let me route it to the right place”
  • Lambda is the worker: “I got your text, let me process it”
  • Comprehend is the expert: “I’m really good at understanding language, let me help”

Seeing how they pass data to each other like a well-coordinated team made distributed systems feel less abstract.

Codes

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Smart Sentiment & Content Analyzer</title>

  <!-- Optional: pretty font; safe to remove if you don't want external calls -->
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">

  <style>
    :root{
      --bg: #0b1020;
      --panel: #11182b;
      --text: #e6ecff;
      --muted:#93a0c8;
      --brand:#7c9bff;
      --accent:#46e0b3;
      --danger:#ff6b6b;
      --chip:#1a2442;
      --border:#223058;
      --shadow: 0 10px 30px rgba(0,0,0,.35);
    }
    /* light mode tweaks will invert these via .light class on body */
    body.light{
      --bg:#f6f8ff;
      --panel:#ffffff;
      --text:#0b1020;
      --muted:#4b597d;
      --brand:#4b6bff;
      --accent:#0bbf8a;
      --danger:#e35656;
      --chip:#eef2ff;
      --border:#e5e8f5;
      --shadow: 0 10px 30px rgba(16,26,71,.08);
    }

    *{box-sizing:border-box}
    html,body{height:100%}
    body{
      margin:0;
      font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
      background: radial-gradient(1200px 800px at 80% -10%, rgba(124,155,255,.20), transparent 60%),
                  radial-gradient(900px 700px at -10% 110%, rgba(70,224,179,.12), transparent 60%),
                  var(--bg);
      color:var(--text);
      line-height:1.55;
    }

    .wrap{
      max-width: 980px;
      margin: clamp(16px, 4vw, 48px) auto;
      padding: 0 16px 48px;
    }

    header{
      display:flex;align-items:center;justify-content:space-between;
      gap:16px;margin-bottom:18px;
    }
    .brand{
      display:flex;align-items:center;gap:14px;
    }
    .logo{
      width:42px;height:42px;border-radius:12px;
      background:
        radial-gradient(18px 18px at 30% 30%, #9ad9ff, transparent 70%),
        radial-gradient(22px 22px at 70% 70%, #7cffc8, transparent 70%),
        linear-gradient(135deg, #5C7CFF 0%, #46E0B3 100%);
      box-shadow: var(--shadow);
    }
    h1{
      font-size: clamp(20px, 2.2vw, 28px);
      margin:0;
    }
    .sub{
      color:var(--muted);font-size:.95rem;margin-top:4px;
    }

    .toggle{
      border:1px solid var(--border);
      background:var(--panel);
      color:var(--text);
      border-radius:12px;padding:10px 14px;cursor:pointer;
      box-shadow: var(--shadow);
    }
    .toggle:hover{opacity:.9}

    .card{
      background: var(--panel);
      border:1px solid var(--border);
      border-radius: 18px;
      box-shadow: var(--shadow);
      overflow: clip;
    }

    .section{
      padding:20px 20px 16px 20px;
      border-bottom:1px solid var(--border);
    }
    .section:last-child{border-bottom:none}

    textarea{
      width:100%; min-height:130px; resize:vertical;
      background: transparent; color: var(--text);
      border:1px solid var(--border);
      border-radius:14px; padding:14px 14px 40px 14px;
      outline: none;
    }
    textarea:focus{border-color:var(--brand); box-shadow:0 0 0 3px rgba(124,155,255,.25)}

    .row{display:flex;gap:12px;align-items:center;justify-content:space-between;margin-top:10px}

    .muted{color:var(--muted)}
    .count{font-size:.9rem}

    .btn{
      appearance:none; border:none; cursor:pointer;
      background: linear-gradient(135deg, var(--brand), var(--accent));
      color:#00111b; font-weight:700;
      padding:12px 18px;border-radius:12px;min-width:128px;
      box-shadow: 0 10px 20px rgba(76,109,255,.22);
      transition: transform .05s ease, box-shadow .15s ease, opacity .15s ease;
    }
    .btn:hover{transform: translateY(-1px)}
    .btn:disabled{opacity:.6; cursor:not-allowed; transform:none}

    .chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
    .chip{
      background:var(--chip); color:var(--text); border:1px solid var(--border);
      padding:6px 10px;border-radius:999px; font-size:.9rem
    }

    .result-grid{
      display:grid; gap:16px;
      grid-template-columns: repeat(12, 1fr);
    }
    .col-4{grid-column: span 4}
    .col-8{grid-column: span 8}
    @media (max-width: 880px){
      .col-4,.col-8{grid-column: 1 / -1}
    }

    .panel{
      border:1px solid var(--border);
      background: linear-gradient(180deg, rgba(124,155,255,.06), transparent 60%), var(--panel);
      border-radius:14px; padding:14px 14px 10px;
      height:100%;
    }
    .panel h3{margin:0 0 8px 0; font-size:1.02rem}

    .sentiment-badge{
      display:inline-flex;align-items:center;gap:10px;
      border-radius:999px; padding:8px 12px; font-weight:700;
      color:#00111b; background:#dbe4ff; border:1px solid var(--border)
    }
    .sentiment-badge[data-kind="Positive"]{background:#c9f7e8}
    .sentiment-badge[data-kind="Negative"]{background:#ffd6d6}
    .sentiment-badge[data-kind="Neutral"] {background:#e9eefc}
    .sentiment-badge[data-kind="Mixed"]   {background:#ffeec7}

    .bars{display:grid;gap:8px;margin-top:12px}
    .bar{display:grid;grid-template-columns:110px 1fr;align-items:center;gap:10px}
    .track{height:10px;border-radius:999px;background:rgba(124,155,255,.18);overflow:hidden;border:1px solid var(--border)}
    .fill{height:100%;background:linear-gradient(90deg, var(--brand), var(--accent))}

    table{
      width:100%; border-collapse:collapse; font-size:.95rem; margin-top:6px
    }
    th,td{border-bottom:1px solid var(--border); padding:10px 8px; text-align:left}
    th{color:var(--muted); font-weight:600}
    tr:last-child td{border-bottom:none}

    .toast{
      position:fixed; inset:auto 16px 16px auto; z-index:9999;
      background:var(--panel); border:1px solid var(--border);
      color:var(--text); padding:12px 14px; border-radius:12px; box-shadow:var(--shadow); display:none;
    }
    .toast.show{display:block; animation:pop .18s ease}
    @keyframes pop{from{transform:translateY(6px);opacity:.0}to{transform:none;opacity:1}}
    footer{color:var(--muted); font-size:.9rem; margin-top:16px}
    a{color:var(--brand);text-decoration:none}
  </style>
</head>
<body class="light">
  <div class="wrap">
    <header>
      <div class="brand">
        <div class="logo" aria-hidden="true"></div>
        <div>
          <h1>Smart Sentiment & Content Analyzer</h1>
          <div class="sub">Paste any short text—get sentiment, key phrases, and named entities in seconds.</div>
        </div>
      </div>
      <button class="toggle" id="themeBtn" aria-label="Toggle theme">🌙 Dark / Light</button>
    </header>

    <div class="card">
      <div class="section">
        <label for="inputText" class="muted">Your text</label>
        <div style="position:relative;margin-top:6px">
          <textarea id="inputText" placeholder="e.g., I absolutely love the camera but the battery life is disappointing."></textarea>
          <div class="row">
            <div class="muted count" id="charCount">0 characters</div>
            <div style="display:flex; gap:10px; align-items:center">
              <button class="btn" id="analyzeBtn">Analyze</button>
            </div>
          </div>
        </div>
      </div>

      <div class="section" id="resultsSection" style="display:none">
        <div class="result-grid">
          <div class="col-4">
            <div class="panel">
              <h3>Overall sentiment</h3>
              <div id="sentimentBadge" class="sentiment-badge" data-kind="Neutral">Neutral</div>
              <div class="bars">
                <div class="bar">
                  <div class="muted">Positive</div>
                  <div class="track"><div class="fill" id="pPos" style="width:0%"></div></div>
                </div>
                <div class="bar">
                  <div class="muted">Negative</div>
                  <div class="track"><div class="fill" id="pNeg" style="width:0%"></div></div>
                </div>
                <div class="bar">
                  <div class="muted">Neutral</div>
                  <div class="track"><div class="fill" id="pNeu" style="width:0%"></div></div>
                </div>
                <div class="bar">
                  <div class="muted">Mixed</div>
                  <div class="track"><div class="fill" id="pMix" style="width:0%"></div></div>
                </div>
              </div>
            </div>
          </div>

          <div class="col-8">
            <div class="panel">
              <h3>Key phrases</h3>
              <div id="phrases" class="chips"></div>
              <h3 style="margin-top:14px">Entities</h3>
              <table id="entitiesTbl">
                <thead><tr><th>Text</th><th>Type</th></tr></thead>
                <tbody></tbody>
              </table>
            </div>
          </div>
        </div>
      </div>

    </div>

    <footer>
      Tip: press <b>Ctrl / Cmd + Enter</b> to analyze quickly. &nbsp;•&nbsp;
      <a href="#" id="sample">Try a sample</a>
    </footer>
  </div>

  <div class="toast" id="toast"></div>

  <script>
    // 🔧 Replace with your Invoke URL if it changes later.
    const API_URL = 'https://l36mrksep9.execute-api.eu-north-1.amazonaws.com/prod/analyze';

    const input = document.getElementById('inputText');
    const btn = document.getElementById('analyzeBtn');
    const badge = document.getElementById('sentimentBadge');
    const phrasesWrap = document.getElementById('phrases');
    const entitiesTbl = document.querySelector('#entitiesTbl tbody');
    const section = document.getElementById('resultsSection');
    const charCount = document.getElementById('charCount');
    const toast = document.getElementById('toast');
    const themeBtn = document.getElementById('themeBtn');
    const sample = document.getElementById('sample');

    function showToast(msg){ toast.textContent = msg; toast.classList.add('show'); setTimeout(()=>toast.classList.remove('show'), 2800); }

    function fmtPct(x){ return (x*100).toFixed(1)+'%'; }
    function setBar(id, val){ document.getElementById(id).style.width = Math.min(100, Math.max(0, val*100)) + '%'; }

    function setBadge(kind){
      badge.setAttribute('data-kind', kind);
      badge.textContent = kind;
    }

    function render(result){
      // sentiment + scores
      setBadge(result.sentiment);
      const s = result.sentimentScores || {};
      setBar('pPos', s.Positive || 0);
      setBar('pNeg', s.Negative || 0);
      setBar('pNeu', s.Neutral || 0);
      setBar('pMix', s.Mixed || 0);

      // key phrases
      phrasesWrap.innerHTML = '';
      (result.keyPhrases || []).slice(0, 30).forEach(t=>{
        const span = document.createElement('span');
        span.className = 'chip';
        span.textContent = t;
        phrasesWrap.appendChild(span);
      });

      // entities
      entitiesTbl.innerHTML = '';
      (result.entities || []).slice(0, 80).forEach(e=>{
        const tr = document.createElement('tr');
        const td1 = document.createElement('td'); td1.textContent = e.Text;
        const td2 = document.createElement('td'); td2.textContent = e.Type;
        tr.append(td1, td2); entitiesTbl.appendChild(tr);
      });

      section.style.display = 'block';
    }

    async function analyze(){
      const text = (input.value || '').trim();
      if(!text){ showToast('Please enter some text.'); input.focus(); return; }

      btn.disabled = true; const original = btn.textContent; btn.textContent = 'Analyzing…';

      try{
        const res = await fetch(API_URL, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ text })
        });

        const json = await res.json().catch(()=>({}));
        if(!res.ok){
          console.error('API error', res.status, json);
          showToast('API error: ' + (json.error || res.status));
        }else{
          render(json);
        }
      }catch(err){
        console.error(err);
        showToast('Network error. Check the API URL & CORS.');
      }finally{
        btn.disabled = false; btn.textContent = original;
      }
    }

    // events
    btn.addEventListener('click', analyze);
    input.addEventListener('input', ()=>{ charCount.textContent = `${input.value.length} characters`; });
    input.addEventListener('keydown', (e)=>{ if((e.metaKey || e.ctrlKey) && e.key==='Enter'){ analyze(); }});
    themeBtn.addEventListener('click', ()=>{ document.body.classList.toggle('light'); });
    sample.addEventListener('click', (e)=>{
      e.preventDefault();
      input.value = "I absolutely love the new phone’s camera. The screen is gorgeous, but the battery life is disappointing and the price feels high.";
      input.dispatchEvent(new Event('input'));
    });

    // init
    charCount.textContent = '0 characters';
  </script>
</body>
</html>

lambda_function.py

import json
import os
import boto3
import traceback

REGION = os.getenv("COMPREHEND_REGION", "eu-west-1")
comprehend = boto3.client("comprehend", region_name=REGION)

CORS_ORIGIN = os.getenv("CORS_ORIGIN", "*")

def _resp(status, body):
    return {
        "statusCode": status,
        "headers": {
            "Access-Control-Allow-Origin": CORS_ORIGIN,
            "Access-Control-Allow-Headers": "Content-Type",
            "Access-Control-Allow-Methods": "OPTIONS,POST",
            "Content-Type": "application/json"
        },
        "body": json.dumps(body)
    }

def lambda_handler(event, context):
    if event.get("httpMethod") == "OPTIONS":
        return _resp(200, {"ok": True})
    try:
        body = {}
        if "body" in event and event["body"]:
            body = json.loads(event["body"])
        text = (body.get("text") or "").strip()
        if not text:
            return _resp(400, {"error": "Missing 'text' in body"})

        s = comprehend.detect_sentiment(Text=text, LanguageCode="en")
        kp = comprehend.detect_key_phrases(Text=text, LanguageCode="en")
        ent = comprehend.detect_entities(Text=text, LanguageCode="en")

        result = {
            "sentiment": s["Sentiment"],
            "sentimentScores": s["SentimentScore"],
            "keyPhrases": [p["Text"] for p in kp.get("KeyPhrases", [])],
            "entities": [{"Text": e["Text"], "Type": e["Type"]} for e in ent.get("Entities", [])]
        }
        return _resp(200, result)
    except Exception as e:
        print("Error:", e)
        print(traceback.format_exc())  # full stack in CloudWatch
        return _resp(500, {"error": "Internal error"})

메타데이터
post_id
0a3bc5edd280
slug
i-built-a-serverless-smart-sentiment-content-analyser-on-aws-zero-cost-0a3bc5edd280
url
https://medium.com/@sachinthaheshan94/i-built-a-serverless-smart-sentiment-content-analyser-on-aws-zero-cost-0a3bc5edd280
canonical_url
https://medium.com/@sachinthaheshan94/i-built-a-serverless-smart-sentiment-content-analyser-on-aws-zero-cost-0a3bc5edd280
author_url
https://medium.com/@sachinthaheshan94
status
ok
fetched_at
2026-08-21 20:41:42