Building a Likes Tracking API with Val.town

I needed a simple way to track likes across different pieces of content without setting up a full backend infrastructure. Val.town's serverless platform seemed perfect for this - it handles the hosting, database, and scaling automatically.

The result is a straightforward API that lets you track likes for any content using unique string identifiers. Whether it's blog posts, products, or comments, you just need a unique ID.

The Architecture

The API runs on Val.town's Deno runtime using the Hono web framework. It's surprisingly minimal - the entire backend fits in a few files:

  • Database layer using Val.town's hosted SQLite
  • API routes with CORS support for cross-origin requests
  • Documentation page that doubles as an interactive testing interface

The database schema is deliberately simple:

CREATE TABLE likes_by_id_v1 (
  id TEXT PRIMARY KEY,
  likes INTEGER DEFAULT 1,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)

Each piece of content gets a row identified by a string ID. When someone likes it, we either create a new row or increment the existing count.

API Design

The API has three main endpoints:

// Record a like
POST /api/like
{ "id": "post-123" }

// Get like count for specific content
GET /api/likes/post-123

// Get all like data (for admin/stats)
GET /api/likes

Each endpoint returns the current like count, making it easy to update UI in real-time. CORS is enabled for all origins, so you can call it from any website.

Quick Setup

Add likes to any webpage in 2 steps:

  1. Add the script to your page
<script src="https://jamesllllllllll--019894a514b47659ac14319f8b619533.web.val.run/script.js"></script>
  1. Add a likes widget anywhere on your page
<div data-likes-id="your-unique-id">
  <button class="likes-button">❤️ <span class="likes-count">0</span></button>
</div>
  1. Done!
  • Loads current like count automatically
  • Handles button clicks to record likes
  • Updates count in real-time
  • Supports multiple IDs on one page

Customization

  • Multiple widgets: Use different data-likes-id values for each widget
  • Styling: Apply CSS to .likes-count and .likes-button classes
  • Button content: Use any text, emoji, or icon inside the button

Live Example

Try it out (ID: "demo-embedded"). Click the heart to test it.

Example Usage

If you don't want to use the example above, you can implement it yourself:

// Get current likes for a post
fetch('https://jamesllllllllll--019894a514b47659ac14319f8b619533.web.val.run/api/likes/post-123')
  .then(res => res.json())
  .then(data => {
    document.querySelector('.likes-count').textContent = data.likeCount;
  });

// Record a like
fetch('https://jamesllllllllll--019894a514b47659ac14319f8b619533.web.val.run/api/like', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ id: 'post-123' })
})
.then(res => res.json())
.then(data => {
  document.querySelector('.likes-count').textContent = data.likeCount;
});

Dynamic ID from page content

// Get the post ID from your page (could be from URL, data attribute, etc.)
const postId = document.querySelector('[data-post-id]').dataset.postId;
// or const postId = window.location.pathname.split('/').pop();

// Get likes for this specific post
fetch(`https://jamesllllllllll--019894a514b47659ac14319f8b619533.web.val.run/api/likes/${postId}`)
  .then(res => res.json())
  .then(data => {
    document.querySelector('.likes-count').textContent = data.likeCount;
  });

// Record a like for this post
fetch('https://jamesllllllllll--019894a514b47659ac14319f8b619533.web.val.run/api/like', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ id: postId })
})
.then(res => res.json())
.then(data => {
  document.querySelector('.likes-count').textContent = data.likeCount;
});

Why Val.town Works

Val.town eliminates the operational overhead of running a simple API. No server management, no database setup, no deployment pipelines. You write the code, save it, and it's live.

The platform's SQLite integration handles persistence without requiring connection management or ORM setup. The Deno runtime means modern JavaScript/TypeScript without Node.js complexity.

For small APIs that need to be reliable but not complex, it's an excellent choice. The constraints actually make the code cleaner - you focus on the core functionality rather than infrastructure concerns.

The SQLite storage is generous for this use case - Val.town's free tier includes 10MB of database storage, which would accommodate roughly 300,000 liked items before hitting the limit. For most content sites, that's more than enough headroom.

You can see the API in action with the like button below - it's powered by this same Val! You can also check out the Val.town project page for the full documentation, and you can remix it to host it yourself.

Building a Likes Tracking API with Val.town - James Keezer