DEV Community

Cover image for How I Built a Free AI Prompt Library With Node.js and PostgreSQL — From Zero to Live in 30 Days
Gowrishankar Rangasamy
Gowrishankar Rangasamy

Posted on

How I Built a Free AI Prompt Library With Node.js and PostgreSQL — From Zero to Live in 30 Days

If you have been following my Dev.to articles, you know I built Chatzyo — a real-time video chat platform using WebRTC. That project taught me a lot about real-time infrastructure, WebSocket connections, and scaling live traffic.

PromptZyo is completely different. No WebSockets. No real-time anything. Just a well-structured content site built for SEO and passive income.

Here is the honest technical breakdown of how I built it, the decisions I made, and what I would do differently.


The Problem I Was Solving

Most professionals — teachers, lawyers, HR managers, nurses — know that ChatGPT and Claude can help them. But they keep getting generic, useless results.

The problem is not the AI tool.The problem is the prompt.

A vague prompt: "write a lesson plan "Result: generic, unusable output

A specific prompt: "write a 45-minute lesson plan for Grade 5 students on
multiplying fractions for the first time, for a class that excels at whole number operations but has never seen fraction multiplication"
Result: genuinely useful first draft

Nobody had built a library of professionally structured prompts organized by profession. So I built PromptZyo — a free AI prompt library for professionals.


Why I Chose This Tech Stack

Node.js + Express

I know Node well from Chatzyo. For a content-heavy site with PostgreSQL queries and server-side rendering, Express is fast, lightweight, and does exactly what I need.

No need for a heavy framework. Express + EJS handles the routing and
templating cleanly.

EJS Over React

This was a deliberate decision.For Chatzyo I used React for the interactive UI. For PromptZyo — a content library where SEO matters more than interactivity — server-side rendering with EJS is the right choice.

For a prompt library where I need 150+ pages indexed quickly — EJS wins.

PostgreSQL Over MongoDB

My initial instinct was MongoDB — I know it well and prompts seem like
document data.

But the relational structure changed my mind:

This is relational data. PostgreSQL handles it cleanly with foreign keys and JOIN queries. MongoDB would have required manual relationship management.

The query that convinced me:

SELECT p.*, 
       pr.name as profession_name,
       pr.slug as profession_slug,
       c.name as category_name
FROM prompts p
LEFT JOIN professions pr ON p.profession_id = pr.id
LEFT JOIN categories c ON p.category_id = c.id
WHERE pr.slug = $1 AND p.is_active = true
ORDER BY p.created_at DESC
Enter fullscreen mode Exit fullscreen mode

Clean, fast, readable. PostgreSQL was the right call.

Railway for Hosting

For Chatzyo I use a different hosting setup. For PromptZyo I chose Railway for a few reasons:

  • PostgreSQL and Node.js in the same project
  • Git-based deployments — push to GitHub, Railway deploys automatically
  • US East (Virginia) region for US audience
  • Reasonable pricing for a new project

The one gotcha: Railway's internal networking only works within the same project. My PostgreSQL and Node app are in the same Railway project — so the internal connection string works. If they were in different projects, I would need the public proxy URL which has higher latency.

Cloudflare CDN

Cloudflare sits in front of Railway and handles:

  • SSL/HTTPS (free)
  • CDN caching for static assets
  • DDoS protection
  • DNS management

Setup was straightforward — point Cloudflare nameservers at Namecheap, add CNAME record pointing to Railway's deployment URL, done.


Database Schema Design

The schema has 6 main tables:

-- Core content tables
professions (id, name, slug, description, icon, is_active)
categories (id, name, slug, profession_id)
prompts (id, title, slug, content, profession_id, 
         category_id, ai_tool, difficulty, 
         copy_count, is_active, created_at)

-- User features (built but not launched yet)
users (id, google_id, email, name, created_at)
saved_prompts (user_id, prompt_id, saved_at)

-- Blog
blog_posts (id, title, slug, excerpt, content, 
            author, category, is_published, 
            published_at, reading_time)

-- Sessions
session (sid, sess, expire)
Enter fullscreen mode Exit fullscreen mode

The copy_count column tracks how many times each prompt has been copied. Every time a user clicks Copy Prompt, it fires a POST to
/api/track-copy/:id which increments the count.

router.post('/track-copy/:id', async (req, res) => {
  try {
    await pool.query(
      'UPDATE prompts SET copy_count = copy_count + 1 
       WHERE id = $1',
      [req.params.id]
    );
    res.json({ success: true });
  } catch (err) {
    console.error(err);
    res.json({ error: 'Failed to track' });
  }
});
Enter fullscreen mode Exit fullscreen mode

Simple but effective engagement tracking.


The SEO Architecture

This is where PromptZyo differs most from Chatzyo. Chatzyo's traffic came from localized pages. PromptZyo's traffic will come from a different content structure.

URL Structure

Each URL is a crawlable page with unique
title, meta description, and canonical tag.

Schema Markup

Every page type has appropriate schema:

// Profession pages
{
  "@type": "CollectionPage",
  "numberOfItems": prompts.length,
  "breadcrumb": { ... }
}

// Individual prompt pages  
{
  "@type": "HowTo",  // for the prompt itself
  "step": [ ... ]
}
{
  "@type": "FAQPage",  // for the FAQ section
  "mainEntity": [ ... ]
}

// Blog posts
{
  "@type": "Article",
  "author": { "@type": "Person", ... },
  "datePublished": "...",
  "dateModified": "..."
}
Enter fullscreen mode Exit fullscreen mode

Schema markup helps Google understand what each page is — and can trigger
rich results in search.

Dynamic Sitemap

The sitemap generates dynamically from the database:

router.get('/', async (req, res) => {
  const professions = await pool.query(
    'SELECT slug FROM professions WHERE is_active = true'
  );
  const prompts = await pool.query(
    'SELECT slug FROM prompts WHERE is_active = true'
  );
  const blogPosts = await pool.query(
    'SELECT slug FROM blog_posts WHERE is_published = true'
  );

  // Build XML dynamically
  let xml = `<?xml version="1.0" encoding="UTF-8"?>
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`;

  // Add all URLs...

  res.setHeader('Content-Type', 'application/xml');
  res.send(xml);
});
Enter fullscreen mode Exit fullscreen mode

Every new prompt or blog post automatically appears in the sitemap. No manual updates needed.

IndexNow for Bing

When a new blog post is published, it automatically notifies Bing via IndexNow:

async function notifyIndexNow(urls) {
  const key = process.env.INDEXNOW_KEY;

  const data = JSON.stringify({
    host: 'promptzyo.com',
    key: key,
    keyLocation: `https://promptzyo.com/${key}.txt`,
    urlList: Array.isArray(urls) ? urls : [urls]
  });

  // POST to api.indexnow.org
  // Bing indexes within hours instead of weeks
}
Enter fullscreen mode Exit fullscreen mode

New content gets indexed by Bing within hours instead of waiting for the crawler to find it organically.


The Content Strategy

121 prompts across 12 professions means 121 individual pages that Google can index. Each page has:

  • Unique title and meta description
  • Introduction paragraph with keywords
  • The prompt itself
  • How to use section
  • When to use section
  • Pro tip
  • Related prompts
  • FAQ section with FAQPage schema

That is a lot of on-page content for what could have been a simple prompt card. But the content depth is what makes each page rankable — not just indexable.

On top of the prompt pages, I am building topical authority through blog articles.

Articles like:

Each article targets a specific high-volume keyword and links back to relevant profession pages and individual prompts.


The Admin Panel

I built a custom admin panel rather than using a CMS. It handles:

  • Add/Edit/Delete prompts
  • Bulk JSON import (for adding 10 prompts at once)
  • Add professions and categories
  • Blog post management (create, edit, publish)
  • Dashboard stats (total prompts, copies, users)

The bulk import route accepts a JSON array:

router.post('/bulk-import', isAdmin, async (req, res) => {
  const { profession_id, ai_tool, 
          difficulty, prompts_json } = req.body;

  const prompts = JSON.parse(prompts_json);

  for (const prompt of prompts) {
    const slug = prompt.title
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/(^-|-$)/g, '');

    await pool.query(`
      INSERT INTO prompts 
        (title, slug, content, profession_id, 
         category_id, ai_tool, difficulty)
      VALUES ($1, $2, $3, $4, $5, $6, $7)
      ON CONFLICT (slug) DO NOTHING
    `, [prompt.title, slug, prompt.content, 
        profession_id, prompt.category_id, 
        ai_tool, difficulty]);
  }

  res.render('admin-bulk', { success: `Added ${added} prompts` });
});
Enter fullscreen mode Exit fullscreen mode

This let me add 10 prompts per profession in one paste rather than one by one.


What Is Different From Building Chatzyo

Building Chatzyo taught me real-time infrastructure. Building PromptZyo
taught me something different:

Content architecture at scale.

With Chatzyo, the technical challenge was keeping WebSocket connections stable and matching users in real time. With PromptZyo, the challenge is making 150+ pages rankable individually while maintaining site-wide
consistency.

Different problems, different solutions, both valuable.


Honest Results After 30 Days

Zero organic traffic at day 30 is completely normal for a new domain. Google takes 3-6 months to trust new sites. I built this expecting
nothing until month 3.

The SEO foundation is solid. Now it is a waiting game combined with consistent content production.


What I Would Do Differently

1. Start the blog earlier

I launched the prompt library first and added the blog later. In hindsight, I should have had 5 blog articles ready before launch. Blog content builds topical authority faster than prompt pages alone.

2. Use a CDN for images from day one

I added Cloudflare later. Should have set it up before launch — it is free and the performance difference is immediate.

3. Build the admin panel first

I added features to the admin panel throughout the build. Building it
comprehensively at the start would have saved time later.


What Is Next


Try It

PromptZyo is completely free. 121 prompts across
12 professions. No account needed.

If you are building something similar — a content library, a tool directory, anything SEO-focused — happy to answer questions in the comments.

And if you read my Chatzyo articles
— thank you for following along. Building two very different products
back to back has been the best technical education I could have given myself.


Built with Node.js, Express, EJS, PostgreSQL, Railway and Cloudflare.

Previous build: How I Built an Anonymous Chat Platform Without Signup Using WebRTC

Top comments (0)