How to Vibe-Code a Certificate Maker with Claude Code and Bannerbear

In this tutorial, you’ll learn how to vibe-code a certificate maker web app with a live preview form using Claude Code and the Bannerbear API.
by Josephine Loo ·

Contents

    Automating certificate generation with a backend script works well when you're the one running it. But what if you want a course admin, an HR manager, or an event coordinator to generate certificates themselves without touching the terminal? You need a simple web frontend.

    Instead of handing off a script and a set of instructions, give them a form to enter a name, select a course, pick a date, and download the certificate. In this tutorial, we'll vibe-code a simple web app using Claude Code and the Bannerbear API.

    Here’s the plan:

    • A Node.js + Express backend that accepts form data and calls the Bannerbear API to generate a certificate image
    • A single-page HTML frontend with a form and a live certificate preview that updates after generation

    How to Vibe-Code a Certificate Maker with Claude Code and Bannerbear - screenshot of the web app

    Everything is scaffolded and wired together using Claude Code with natural language prompts. By the end, you'll have a working web app you can hand off to anyone on your team, similar to Bannerbear's free certificate maker, but self-hosted and fully under your control. Let's get started!

    Pre-requisites

    You will need:

    What is Bannerbear?

    Bannerbear is an automated media generation tool that lets you create custom images, videos, PDFs, and more from design templates. It offers a REST API along with official libraries for Node.js, Ruby, and PHP, so you can integrate image and media generation into any project without building a renderer from scratch.

    To generate images or videos automatically, you create a design template that serves as a blueprint for creating the images and videos in Bannerbear's editor. A template can contain two types of objects:

    • Static objects - These remain the same in every generated image (e.g., a company logo)
    • Dynamic objects - These change based on data you provide (e.g., a recipient's name, a course title, or a date)

    When you call the API, you pass in values for the dynamic objects, and Bannerbear merges them into the template, returning a finished image. This makes it easy to generate thousands of unique, consistently branded assets from a single design. For our certificate maker, each API call will produce one personalized certificate with a different name and course filled in.

    Now that you have a basic understanding of how Bannerbear works, let's set it up.

    Vibe-Coding a Certificate Maker with Claude Code

    Step 1. Create Your Certificate Template in Bannerbear

    Log in to Bannerbear, create a new project, and add a template. You can duplicate the one below or pick one from the template library to get started quickly:

    Bannerbear Certificate with Photo template

    Make sure your template has these dynamic text layers:

    • recipient_name
    • course_title
    • issue_date

    How to Vibe-Code a Certificate Maker with Claude Code and Bannerbear - screenshot of the template in Bannerbear editor

    Once your template is ready, copy your Project API Key and Template ID from the project settings:

    How to Vibe-Code a Certificate Maker with Claude Code and Bannerbear - copy Bannerbear project API key and template ID

    Step 2. Create the Project with Claude Code

    In your terminal/command prompt, run the commands below to create a new folder and start Claude Code in it:

    mkdir certificate-maker && cd certificate-maker
    claude
    

    Now describe the project to Claude Code in one prompt:

    “Create a Node.js Express app with a single HTML page. The page should have a form with fields for recipient name, course title, and issue date. When the user submits the form, the frontend should POST the data to an Express endpoint. The endpoint should call the Bannerbear sync API to generate a certificate image using a template, then return the image URL to the frontend. The frontend should display the image below the form with a download button. Store the Bannerbear API key and template UID in a .env file.”

    If you’re asked to give permission to run some npm commands or make edits to some files, select yes:

    How to Vibe-Code a Certificate Maker with Claude Code and Bannerbear - giving permission to Claude Code

    Claude Code will set up the project and install the required dependencies. Your project structure should look something like this:

    certificate-maker/
      ├── .env ← API key & template UID                                                                                                                                              
      ├── .gitignore                                            
      ├── package.json
      ├── server.js ← Express server
      └── public/
          └── index.html ← Frontend form + result display
    

    Next, add your Bannerbear credentials to .env:

    How to Vibe-Code a Certificate Maker with Claude Code and Bannerbear - adding environment variables

    Step 3. Review the Backend (server.js)

    The Express server needs to do two things: serve the frontend HTML file, and handle the POST request that triggers certificate generation. Here's what Claude Code produced:

    require('dotenv').config();
    const express = require('express');
    const fetch = require('node-fetch');
    const path = require('path');
    
    const app = express();
    app.use(express.json());
    app.use(express.static(path.join(__dirname, 'public')));
    
    app.post('/generate', async (req, res) => {
      const { recipientName, courseTitle, issueDate } = req.body;
    
      if (!recipientName || !courseTitle || !issueDate) {
        return res.status(400).json({ error: 'All fields are required.' });
      }
    
      try {
        const response = await fetch('https://sync.api.bannerbear.com/v2/images', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${process.env.BANNERBEAR_API_KEY}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            template: process.env.BANNERBEAR_TEMPLATE_UID,
            modifications: [
              { name: 'recipient_name', text: recipientName },
              { name: 'course_title', text: courseTitle },
              { name: 'issue_date', text: issueDate },
            ],
          }),
        });
    
        if (!response.ok) {
          const error = await response.json();
          return res.status(response.status).json({ error: error.message || 'Bannerbear API error.' });
        }
    
        const data = await response.json();
        res.json({ imageUrl: data.image_url });
      } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Failed to generate certificate.' });
      }
    });
    
    const PORT = process.env.PORT || 3000;
    app.listen(PORT, () => console.log(`Server running at http://localhost:${PORT}`));
    

    Here’s what the code above does:

    1. express.static serves the public/ folder, so the HTML page is accessible at localhost:3000.
    2. The /generate route destructures recipientName, courseTitle, and issueDate from the request body.
    3. It does a basic validation check before making any API call and returns a 400 immediately if any field is missing.
    4. It calls Bannerbear's sync endpoint using node-fetch. The modifications array maps each camelCase field to the snake_case layer names defined in your Bannerbear template.
    5. If Bannerbear returns a non-OK response, the error is forwarded back to the client with the original status code for debugging.
    6. On success, the image URL is returned as imageUrl in the JSON response.

    Step 4. Review the Frontend (public/index.html)

    The frontend is a single HTML file with an inline form and a JavaScript fetch call. Ask Claude Code to generate it:

    “Write public/index.html with a clean, minimal design. It should have a form with three fields: recipient name, course title, and issue date. On submit, it should POST to /generate and show a loading state. When the response comes back, display the certificate image below the form along with a Download Certificate button.”

    Claude Code should generate code similar to this in the HTML file:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>Certificate Maker</title>
      <style>
        *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    
        body {
          font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
          background: #fafafa;
          color: #111;
          min-height: 100vh;
          display: flex;
          flex-direction: column;
          align-items: center;
          padding: 64px 20px 80px;
        }
    
        header {
          text-align: center;
          margin-bottom: 40px;
        }
    
        header h1 {
          font-size: 1.625rem;
          font-weight: 700;
          letter-spacing: -0.02em;
        }
    
        header p {
          margin-top: 6px;
          font-size: 0.9rem;
          color: #666;
        }
    
        .card {
          background: #fff;
          border: 1px solid #e5e5e5;
          border-radius: 16px;
          padding: 36px 32px;
          width: 100%;
          max-width: 460px;
        }
    
        .field {
          display: flex;
          flex-direction: column;
          gap: 6px;
          margin-bottom: 20px;
        }
    
        .field:last-of-type { margin-bottom: 28px; }
    
        label {
          font-size: 0.8125rem;
          font-weight: 600;
          color: #444;
          letter-spacing: 0.01em;
        }
    
        input {
          padding: 10px 13px;
          border: 1px solid #ddd;
          border-radius: 8px;
          font-size: 0.9375rem;
          color: #111;
          background: #fff;
          outline: none;
          transition: border-color 0.15s, box-shadow 0.15s;
          width: 100%;
        }
    
        input::placeholder { color: #aaa; }
    
        input:focus {
          border-color: #111;
          box-shadow: 0 0 0 3px rgba(0,0,0,0.06);
        }
    
        .btn-primary {
          width: 100%;
          padding: 12px;
          background: #111;
          color: #fff;
          border: none;
          border-radius: 8px;
          font-size: 0.9375rem;
          font-weight: 600;
          cursor: pointer;
          display: flex;
          align-items: center;
          justify-content: center;
          gap: 8px;
          transition: background 0.15s;
        }
    
        .btn-primary:hover { background: #333; }
        .btn-primary:disabled { background: #999; cursor: not-allowed; }
    
        .error {
          display: none;
          font-size: 0.8125rem;
          color: #c0392b;
          background: #fff5f5;
          border: 1px solid #fcc;
          border-radius: 8px;
          padding: 10px 13px;
          margin-bottom: 16px;
        }
    
        /* Spinner */
        .spinner {
          width: 16px;
          height: 16px;
          border: 2px solid rgba(255,255,255,0.4);
          border-top-color: #fff;
          border-radius: 50%;
          animation: spin 0.65s linear infinite;
          flex-shrink: 0;
        }
    
        @keyframes spin { to { transform: rotate(360deg); } }
    
        /* Result */
        #result {
          display: none;
          flex-direction: column;
          align-items: center;
          gap: 16px;
          margin-top: 32px;
          width: 100%;
          max-width: 460px;
        }
    
        #result img {
          width: 100%;
          border-radius: 12px;
          border: 1px solid #e5e5e5;
          display: block;
        }
    
        .btn-download {
          display: inline-flex;
          align-items: center;
          gap: 8px;
          padding: 11px 24px;
          background: #fff;
          color: #111;
          border: 1.5px solid #111;
          border-radius: 8px;
          font-size: 0.9375rem;
          font-weight: 600;
          text-decoration: none;
          transition: background 0.15s, color 0.15s;
        }
    
        .btn-download:hover {
          background: #111;
          color: #fff;
        }
    
        .btn-download svg, .btn-primary svg { flex-shrink: 0; }
      </style>
    </head>
    <body>
      <header>
        <h1>Certificate Maker</h1>
        <p>Fill in the details below to generate a certificate.</p>
      </header>
    
      <div class="card">
        <p class="error" id="error"></p>
    
        <form id="certForm">
          <div class="field">
            <label for="recipientName">Recipient Name</label>
            <input type="text" id="recipientName" placeholder="Jane Doe" autocomplete="off" required />
          </div>
    
          <div class="field">
            <label for="courseTitle">Course Title</label>
            <input type="text" id="courseTitle" placeholder="Introduction to Web Development" autocomplete="off" required />
          </div>
    
          <div class="field">
            <label for="issueDate">Issue Date</label>
            <input type="date" id="issueDate" required />
          </div>
    
          <button type="submit" id="submitBtn">
            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
              <path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"/>
              <path d="M8 4a.75.75 0 01.75.75v2.5h2.5a.75.75 0 010 1.5h-2.5v2.5a.75.75 0 01-1.5 0v-2.5h-2.5a.75.75 0 010-1.5h2.5v-2.5A.75.75 0 018 4z" fill="currentColor"/>
            </svg>
            Generate Certificate
          </button>
        </form>
      </div>
    
      <div id="result">
        <img id="certImage" src="" alt="Generated certificate" />
        <a id="downloadBtn" download="certificate.png">
          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M7.47 10.78a.75.75 0 001.06 0l3.25-3.25a.75.75 0 00-1.06-1.06L8.75 8.44V2.75a.75.75 0 00-1.5 0v5.69L5.28 6.47a.75.75 0 00-1.06 1.06l3.25 3.25z" fill="currentColor"/>
            <path d="M2.5 13.25a.75.75 0 000 1.5h11a.75.75 0 000-1.5h-11z" fill="currentColor"/>
          </svg>
          Download Certificate
        </a>
      </div>
    
      <script>
        const form = document.getElementById('certForm');
        const submitBtn = document.getElementById('submitBtn');
        const errorEl = document.getElementById('error');
        const result = document.getElementById('result');
        const certImage = document.getElementById('certImage');
        const downloadBtn = document.getElementById('downloadBtn');
    
        const IDLE_LABEL = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"/><path d="M8 4a.75.75 0 01.75.75v2.5h2.5a.75.75 0 010 1.5h-2.5v2.5a.75.75 0 01-1.5 0v-2.5h-2.5a.75.75 0 010-1.5h2.5v-2.5A.75.75 0 018 4z" fill="currentColor"/></svg> Generate Certificate`;
        const LOADING_LABEL = `<span class="spinner"></span> Generating…`;
    
        form.addEventListener('submit', async (e) => {
          e.preventDefault();
    
          errorEl.style.display = 'none';
          result.style.display = 'none';
          submitBtn.disabled = true;
          submitBtn.innerHTML = LOADING_LABEL;
    
          const body = {
            recipientName: document.getElementById('recipientName').value.trim(),
            courseTitle: document.getElementById('courseTitle').value.trim(),
            issueDate: document.getElementById('issueDate').value,
          };
    
          try {
            const res = await fetch('/generate', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify(body),
            });
    
            const data = await res.json();
            if (!res.ok) throw new Error(data.error || 'Something went wrong.');
    
            certImage.src = data.imageUrl;
            downloadBtn.href = data.imageUrl;
            result.style.display = 'flex';
            result.scrollIntoView({ behavior: 'smooth', block: 'start' });
          } catch (err) {
            errorEl.textContent = err.message;
            errorEl.style.display = 'block';
          } finally {
            submitBtn.disabled = false;
            submitBtn.innerHTML = IDLE_LABEL;
          }
        });
      </script>
    </body>
    </html>
    

    Step 5. Run the App and Test It

    In your terminal/command prompt, run the command below to start the server:

    node server.js
    

    Open the localhost address (e.g., http://localhost:3000) in your browser. Fill in a name, course, and date, then click “Generate Certificate” :

    How to Vibe-Code a Certificate Maker with Claude Code and Bannerbear - screenshot of the web app wtih unfilled fields

    Within a couple of seconds, your certificate will appear below the form, and ready to be downloaded:

    How to Vibe-Code a Certificate Maker with Claude Code and Bannerbear - screenshot of the web app

    Bonus: Add More Templates

    Right now, the app uses a single fixed template. To let users choose between multiple designs, extend the backend and frontend like this:

    “Update the app to support three certificate templates. Add a template selector to the form with thumbnail previews. Pass the selected template UID to the /generate endpoint.”

    Claude Code will add a visual template picker, update the POST payload, and adjust the backend to use the selected ID so that users can choose a template:

    How to Vibe-Code a Certificate Maker with Claude Code and Bannerbear - screenshot of the web app with template selection

    🐻 Bear Tip: Add more templates to your project from Bannerbear's template library.

    Conclusion

    Building a certificate generator this way is faster than using a drag-and-drop website builder and more flexible. You own the frontend completely, so you can match your brand, embed it in an existing app, or connect it to your own database of recipients.

    Bannerbear handles the hard part—consistent, high-quality image rendering across every certificate, while your frontend handles the user experience. If you’d like to extend this project further, here are some ideas: connect it to a Google Form, trigger certificates automatically when a course is completed, or batch-generate for entire cohorts at once.

    For more on what Bannerbear can automate, check out Bannerbear’s API Reference and other tutorials:

    About the authorJosephine Loo
    Josephine is an automation enthusiast. She loves automating stuff and helping people to increase productivity with automation.

    6 Basic Coding Concepts for Vibe Coding Beginners in 2026

    New to coding but using AI coding tools like Claude Code or GitHub Copilot? This beginner's guide covers the essential coding concepts you need to understand so you can work with AI more effectively, debug easily, and build real things fast.

    How to Generate E-commerce Product Thumbnails with Bannerbear

    Thumbnails can be the key factor in stopping the scroll. Learn how to automate e-commerce product thumbnail generation using Bannerbear, Airtable, and Zapier. Design once, generate at scale—no coding required.

    8 Best MCP Servers for Claude Code Developers in 2026

    Discover the 8 best MCP servers for Claude Code in 2026 that will supercharge your development workflow, including GitHub, Playwright, Supabase, Figma, and more.

    Automate & Scale
    Your Marketing

    Bannerbear helps you auto-generate social media visuals, banners and more with our API and nocode integrations

    How to Vibe-Code a Certificate Maker with Claude Code and Bannerbear
    How to Vibe-Code a Certificate Maker with Claude Code and Bannerbear