<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Sanjay - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Sanjay - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 24 May 2026 22:23:57 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/sanjayxr/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Convert Your Website into an Android App Using Bubblewrap ]]>
                </title>
                <description>
                    <![CDATA[ If you are a web developer who doesn’t know about App Development (like me!), then this article is for you. I’ll teach you how to turn your website into a native app, without new frameworks or languages. You’ll learn how to convert a website to a PWA... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-convert-your-website-into-an-android-app-using-bubblewrap/</link>
                <guid isPermaLink="false">68a4b9d4f2bced8c3a658f5a</guid>
                
                    <category>
                        <![CDATA[ Android ]]>
                    </category>
                
                    <category>
                        <![CDATA[ PWA ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Sanjay ]]>
                </dc:creator>
                <pubDate>Tue, 19 Aug 2025 17:52:20 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1755625913612/bfffd5f9-f4d6-4f8d-aae8-72f5730bd7e9.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you are a web developer who doesn’t know about App Development (like me!), then this article is for you. I’ll teach you how to turn your website into a native app, without new frameworks or languages. You’ll learn how to convert a website to a PWA (Progressive Web App) that you can publish on the Play Store.</p>
<p>First, we’ll turn your website into a Progressive Web App (PWA). Then we'll use a free command-line tool from Google called <strong>Bubblewrap</strong> to package that PWA into an Android app. Let’s get started.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>If you follow along with this tutorial, there are some prerequisites:</p>
<ul>
<li><p>Basic knowledge of web development</p>
</li>
<li><p>Your site should be live to the public, and you’ll need to have access to its source code.</p>
</li>
<li><p>We'll use npm to install the necessary tools, so make sure you have Node.js installed.</p>
</li>
</ul>
<p><strong>Note:</strong> This tutorial is based on a <strong>Vite</strong> project, but the final steps with Bubblewrap are the same for any web framework.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-what-is-a-pwa">What is a PWA?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-bubblewrap">What is Bubblewrap?</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-a-twa-trusted-web-activity">What is a TWA (Trusted Web Activity)?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-twa-verifies-trust">How TWA Verifies Trust</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-step-1-configure-your-pwa-in-vite">Step 1 – Configure Your PWA in Vite</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-create-your-app-icons">Create Your App Icons</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-install-the-vite-pwa-plugin">Install the Vite PWA plugin.</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-the-plugin">Configure the Plugin</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-create-the-android-app">Step 2 – Create the Android App</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-create-a-build-folder">Create a Build Folder</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-install-the-bubblewrap-cli">Install the Bubblewrap CLI</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-initialize-the-project">Initialize the Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-lets-troubleshoot-the-init-command">Let’s troubleshoot the init command</a>.</p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-answer-bubblewrap-questions">Step 3 – Answer Bubblewrap Questions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-build-the-app">Step 4 – Build the App</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-setting-up-twa-validation">Step 5 – Setting Up TWA Validation</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-the-well-known-folder">What is the .well-known folder?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-delegatepermissioncommonhandleallurls">What is delegate_permission/common.handle_all_urls?</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-step-6-optional-customize-the-in-app-experience">Step 6 (Optional) – Customize the In-App Experience</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ol>
<h2 id="heading-what-is-a-pwa">What is a PWA?</h2>
<p>PWA stands for <strong>Progressive Web Application</strong>, and its goal is to make your website look and feel just like a native app. If you’ve visited a website in your browser and seen an install icon that lets you download it to your phone or laptop, you've used a PWA.</p>
<p>But it’s not just about the look and feel. A PWA also has app-like features, such as working offline, sending push notifications, and more.</p>
<p>There are two main components of a PWA.</p>
<ul>
<li><p>The manifest file describes your app, such as its name, icons, start URL, and so on.</p>
</li>
<li><p>A service worker is a background JavaScript file that acts as a proxy. The caching and push notifications are handled by a service file, which runs as a different thread apart from the main thread.</p>
</li>
</ul>
<p>Without these two components, browsers won’t let users download the app locally.</p>
<p>The manifest file and the service worker are like a checklist for the browser. When you visit a website, the browser looks for both of these components. If they are present and correctly configured, the browser knows it's a true PWA and will show the "install" icon, allowing users to download the app locally. Without them, the browser just sees a regular website, and the option to install won't be available.</p>
<h2 id="heading-what-is-bubblewrap">What is Bubblewrap?</h2>
<p>Bubblewrap is a command-line tool made by Google that takes your PWA and turns it into an Android App using a Trusted Web Activity (TWA).</p>
<p>Bubblewrap simplifies the process of creating a TWA, turning a PWA's manifest file into an Android app package (APK or AAB).</p>
<h3 id="heading-what-is-a-twa-trusted-web-activity">What is a TWA (Trusted Web Activity)?</h3>
<p>A Trusted Web Activity (TWA) is a modern Android feature that lets you display your live website full-screen inside an Android app. Basically, it runs the website on the browser, but it doesn’t show the browser address bar on the App. This helps it feel like a native app.</p>
<p>To unlock this full-screen feature, your app needs to be “Trusted“.</p>
<p>This is where the "secret handshake" comes in. Android needs to be sure that the person who built the app and the person who owns the website are the same. Without this proof of ownership, the TWA will run in a fallback mode and show the browser address bar at the top, ruining the native app feel.</p>
<h3 id="heading-how-twa-verifies-trust">How TWA Verifies Trust</h3>
<p>This trust is verified using a system called <strong>Digital Asset Links</strong>. You place a special file on your website (we'll do this in the implementation part) that contains your app's unique digital fingerprint. When a user opens your app, the Android OS checks this file. If the fingerprints match, it grants your app "trusted" status, removes the address bar, and enables other features like deep linking.</p>
<p>You can check this relationship yourself using Google's official testing tool: <a target="_blank" href="https://developers.google.com/digital-asset-links/tools/generator">Digital Asset Links Verifier.</a></p>
<p>Now that you understand the project and tools, let’s start building.</p>
<h2 id="heading-step-1-configure-your-pwa-in-vite">Step 1 – Configure Your PWA in Vite</h2>
<p>The first step is to add the two main components for a PWA: the manifest file and service worker. This is what will allow the browser to recognize it as "installable."</p>
<p>This guide is based on a project built with Vite, which makes this process easy with a special plugin. If you're using a different tool, the concepts are the same, but you'll need to look up different resources about the specific steps for your environment.</p>
<h3 id="heading-create-your-app-icons">Create Your App Icons</h3>
<p>Before we touch any code, we need the icons for our app. Android requires specific sizes for the app's launcher icon (what you see on your home screen) and the splash screen (what you see when the app starts).</p>
<p>You'll need two main sizes: <code>192x192</code> pixels and <code>512x512</code> pixels. You can use this <a target="_blank" href="https://realfavicongenerator.net/">Favicon Generator</a> to generate your logo in the respective sizes. You can upload your main logo, and it will generate all the necessary sizes for you.</p>
<p>Then just download the generated files and place the <code>192x192</code> and <code>512x512</code> files into the <code>public</code> folder of your project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755067586673/f7e06fc2-4b55-4ec3-af05-b2e78bf19273.png" alt="f7e06fc2-4b55-4ec3-af05-b2e78bf19273" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-install-the-vite-pwa-plugin">Install the Vite PWA plugin.</h3>
<p>A PWA requires a manifest file and a service worker. We can create these manually, but this plugin automates that entire process. It will automatically generate a <code>manifest.json</code> and <code>service-worker.js</code> for you every time you build your project.</p>
<pre><code class="lang-bash">npm install vite-plugin-pwa -D
</code></pre>
<h3 id="heading-configure-the-plugin">Configure the Plugin</h3>
<p>In this step, we’ll use this plugin and configure our app's manifest. Edit the <code>vite.config.ts</code> file. This configuration will tell the plugin what to name your app, which icons to use, and so on.</p>
<p>In <code>vite.config.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineConfig({
  plugins: [
    VitePWA({
      registerType: <span class="hljs-string">"autoUpdate"</span>,   
      manifest: {
        name: <span class="hljs-string">"your app name"</span>,
        short_name: <span class="hljs-string">"your app short name"</span>,
        description: <span class="hljs-string">"write any description"</span>,
        theme_color: <span class="hljs-string">"#0d1117"</span>,
        background_color: <span class="hljs-string">"#ffffff"</span>,
        display: <span class="hljs-string">"standalone"</span>,
        start_url: <span class="hljs-string">"/"</span>,
        icons: [
          {
            src: <span class="hljs-string">"/web-app-manifest-192x192.png"</span>,
            sizes: <span class="hljs-string">"192x192"</span>,
            <span class="hljs-keyword">type</span>: <span class="hljs-string">"image/png"</span>,
          },
          {
            src: <span class="hljs-string">"/web-app-manifest-512x512.png"</span>,
            sizes: <span class="hljs-string">"512x512"</span>,
            <span class="hljs-keyword">type</span>: <span class="hljs-string">"image/png"</span>,
          },
        ],
      },
    }),
  ]
</code></pre>
<p>Now, when you run <code>npm run build</code>, the plugin will automatically generate the manifest and service worker files for you. With that done, deploy the changes. Now your website is a PWA.</p>
<h2 id="heading-step-2-create-the-android-app">Step 2 – Create the Android App</h2>
<p>Now that your website is a PWA, let’s use Bubblewrap to package it into an Android app.</p>
<h3 id="heading-create-a-build-folder">Create a Build Folder</h3>
<p>Create a dedicated folder for your Android project files. In your project's root, create a new folder. I'll call mine <code>android</code>.</p>
<pre><code class="lang-plaintext">project/
├── client/
├── server/
└── android/
</code></pre>
<p>Now navigate to the new folder that you created.</p>
<h3 id="heading-install-the-bubblewrap-cli">Install the Bubblewrap CLI</h3>
<pre><code class="lang-bash">npm install -g @bubblewrap/cli
</code></pre>
<h3 id="heading-initialize-the-project">Initialize the Project</h3>
<p>Next, run the <code>init</code> command. Bubblewrap will connect to your live website, read the <code>manifest.webmanifest</code> file that Vite created, and use that information to generate a basic Android project.</p>
<pre><code class="lang-bash">bubblewrap init --manifest=https://your-website-domain/manifest.webmanifest
</code></pre>
<p>Run the command, replacing <code>your-website-domain</code> with your actual URL:</p>
<h3 id="heading-lets-troubleshoot-the-init-command">Let’s troubleshoot the <code>init</code> command</h3>
<p>As you run the <code>init</code> command, Bubblewrap will need two key software packages: the <strong>Java Development Kit (JDK)</strong> and the <strong>Android SDK</strong>. It will offer to install them for you.</p>
<h4 id="heading-jdk-setup">JDK setup:</h4>
<pre><code class="lang-bash">? Do you want Bubblewrap to install the JDK (recommended)?
  (Enter <span class="hljs-string">"No"</span> to use your own JDK 17 installation) (Y/n)
</code></pre>
<p>In my case, when I let Bubblewrap install the JDK, the process downloaded the files but then failed at the "decompressing" step. If you face this same problem, don't worry! The fix is to install it manually.</p>
<ul>
<li><p>Say <strong>No</strong> to the prompt.</p>
</li>
<li><p>Download the recommended version (usually JDK 17) from a source like <a target="_blank" href="https://adoptium.net/temurin/releases/?version=17">Adoptium</a>.</p>
</li>
<li><p>Install it and set up your system's environment variables to include the JDK's <code>bin</code> path. If you’re not sure how to set environment variables, you can check out this site: <a target="_blank" href="https://www.c-sharpcorner.com/article/how-to-addedit-path-environment-variable-in-windows-11/">Set Environment Variables</a>.</p>
</li>
<li><p>When Bubblewrap asks for the path, provide it directly, such as <code>C:\java\jdk-17.0.16.8-hotspot</code>.</p>
</li>
</ul>
<h4 id="heading-android-sdk-setup">Android SDK setup:</h4>
<p>Once the JDK is set up successfully, the next step is to configure the Android SDK.</p>
<pre><code class="lang-bash">? Do you want Bubblewrap to install the Android SDK (recommended)?
  (Enter <span class="hljs-string">"No"</span> to use your own Android SDK installation) (Y/n)
</code></pre>
<p>Since I didn't have the Android SDK, I let Bubblewrap handle this by selecting <strong>Yes</strong>. I didn't face any problems here.</p>
<p>If you face any problem in setting up on Android SDK, just set it up manually and give the path, just like the JDK setup.</p>
<h2 id="heading-step-3-answer-bubblewrap-questions">Step 3 – Answer Bubblewrap Questions</h2>
<p>After the SDK is set up, Bubblewrap will ask a bunch of questions to configure your app. This information is used to create the <code>twa-manifest.json</code> file, which is the blueprint for your App.</p>
<pre><code class="lang-plaintext">Domain: Press Enter (auto-filled from your manifest)

Application name: Your full app name

Application ID: (e.g, chat.yourapp.twa)

Display mode: standalone

Orientation: portrait

Status bar color: Press Enter (accepts default)

Splash screen color: Press Enter (accepts default)

Icon URL: Press Enter (accepts default)

Include support for Play Billing?: Type Y if your app uses Google Play in-app purchases. Otherwise, N

Request geolocation permission?: Type Y if your app needs location access. Otherwise, N
</code></pre>
<p>In these questions, the important part is the key store and the key.</p>
<pre><code class="lang-plaintext">First and Last names: Your full name

Organizational Unit: Developer or anything

Organization: Your organization name

Country (2-letter code): Your country code

Password for key store: Enter a new password

Password for key: Re-enter the same password
</code></pre>
<p><strong>Note:</strong> These passwords for both the key store and key should be the same, or else it will throw an error. <strong>Refer to this issue:</strong> <a target="_blank" href="https://github.com/GoogleChromeLabs/bubblewrap/issues/713">Bubblewrap Issue</a>.</p>
<h2 id="heading-step-4-build-the-app">Step 4 – Build the App</h2>
<pre><code class="lang-bash">bubblewrap build --universalApk
</code></pre>
<p>This command starts building your application. Here, the flag <code>universalApk</code> will produce the <code>.apk</code> and <code>.abb</code>. If you’re going to publish your application in the Play Store, upload the <code>.abb</code> file to the Play Store. For our testing, we need an APK file, so this flag <code>universalApk</code> will produce both files. If we didn't give this flag, it would only give us <code>.abb</code>.</p>
<h2 id="heading-step-5-setting-up-twa-validation">Step 5 – Setting Up TWA Validation</h2>
<p>Once the build is done, you’ll get the APK. Transfer it to your phone and test it. When you open the app, you’ll see the browser address bar. This is because we haven't set up the "trust" between your app and your website yet. Let's fix that now.</p>
<p>In your frontend project, go to the <code>public</code> folder, create a new folder called <code>.well-known</code>, and inside that, create a file called <code>assetlinks.json</code>.</p>
<pre><code class="lang-bash">frontend/
├── public/
    ├── .well-known/
        └── assetlinks.json
</code></pre>
<h3 id="heading-what-is-the-well-known-folder">What is the <code>.well-known</code> folder?</h3>
<p>A well-known folder is used to store files that define configurations for protocols, as it’s used for external sources to find the validation for your website. In our case, our app checks the well-known folder from our website and verifies the validation.</p>
<p>Paste the following into <code>assetlinks.json</code>:</p>
<pre><code class="lang-json">[
  {
    <span class="hljs-attr">"relation"</span>: [<span class="hljs-string">"delegate_permission/common.handle_all_urls"</span>],
    <span class="hljs-attr">"target"</span>: {
      <span class="hljs-attr">"namespace"</span>: <span class="hljs-string">"android_app"</span>,
      <span class="hljs-attr">"package_name"</span>: <span class="hljs-string">"chat.yourapp.twa"</span>,
      <span class="hljs-attr">"sha256_cert_fingerprints"</span>: [
       <span class="hljs-string">"your_sha256_fingerprint"</span>
      ]
    }
  }
]
</code></pre>
<h3 id="heading-what-is-delegatepermissioncommonhandleallurls">What is <code>delegate_permission/common.handle_all_urls</code>?</h3>
<p>This is a special flag that opens all the links from the app instead of the domain. Simply put, it acts as a deeplink. After you install the app, if you click your website link from WhatsApp or from somewhere, it will open your app instead of opening in a browser, acting as a deeplink.</p>
<p>The <code>package_name</code> field should be the <code>packageId</code>, which you can get from your Android build folder in <code>twa-manifest.json</code>.</p>
<p>Now, get your fingerprints. Run the following command to do so:</p>
<pre><code class="lang-bash">keytool -list -v -keystore android.keystore -<span class="hljs-built_in">alias</span> android
</code></pre>
<p>The alias name should be the value that you created. Once you enter this command, it’ll ask for the key store password. Enter that, and you’ll get your <code>SHA256</code> fingerprint. Copy that and paste it into the <code>assetslinks.json</code> file in the <code>sha256_cert_fingerprints</code> array. Now push these changes to production. You can verify the validation in <a target="_blank" href="https://developers.google.com/digital-asset-links/tools/generator">Digital Asset Links</a></p>
<p>That’s it! Now you can install the app and test it.</p>
<h2 id="heading-step-6-optional-customize-the-in-app-experience"><strong>Step 6 (Optional) – Customize the In-App Experience</strong></h2>
<p>Now, additionally, there will be some cases where we want to show different content to users on the website vs the mobile app. Can we do that? Yes!</p>
<p>In your Android build folder, in <code>twa-manifest.json</code>, there will be a field called <code>startUrl</code>. If not, add it and add the value  <code>"startUrl": "/?twa=true"</code>. The <code>startUrl</code> is the entry point. I have a query parameter of value <code>twa=true</code>.</p>
<p>Run the build again with <code>bubblewrap build --universalApk</code>.</p>
<p>Now, if you open your app, it will open the app with the entry URL as <code>yourwebsitedomain.com/?twa=true</code>.</p>
<p>In your frontend:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> twaParam = queryParams.get(<span class="hljs-string">"twa"</span>);

<span class="hljs-keyword">const</span> [isTwa, setIsTwa] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-function">() =&gt;</span> {
   <span class="hljs-keyword">return</span> <span class="hljs-built_in">localStorage</span>.getItem(<span class="hljs-string">"isTwa"</span>) === <span class="hljs-string">"true"</span>;
});

useEffect(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">if</span> (twaParam === <span class="hljs-string">"true"</span>) {
    <span class="hljs-built_in">localStorage</span>.setItem(<span class="hljs-string">"isTwa"</span>, <span class="hljs-string">"true"</span>); <span class="hljs-comment">// set the value to local storage</span>
    setIsTwa(<span class="hljs-literal">true</span>);
  }
}, [twaParam]);
</code></pre>
<pre><code class="lang-typescript"> {isTwa? (
    &lt;Link to=<span class="hljs-string">"/contact"</span> className=<span class="hljs-string">"underline hover:text-primary"</span>&gt;
       Contact
    &lt;/Link&gt; 
  ) : (
     &lt;Link to=<span class="hljs-string">"/download"</span> className=<span class="hljs-string">"underline hover:text-primary"</span>&gt;
       Download App
      &lt;/Link&gt;
  )}
</code></pre>
<p>In the code above, we check for the <code>twa=true</code> query parameter in the URL. If it's present, we save that information to local storage, and then we conditionally render the content for the user.</p>
<p>That's it. We have created an App.</p>
<p>If you want to change any name, colour, or splash screen, you can change it in <code>twa-manifest.json</code> and run the build again.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>Bubblewrap is only for Android. If you want the app to support cross-platform, there are some other platforms, like Capacitor, which I’ll write about in another article.</p>
<p>By the way, you can check out the App that I made using Bubblewrap here: <a target="_blank" href="https://strangertalk.chat/download">Stranger Talk</a>.</p>
<p>If there are any mistakes or you have any questions, contact me on <a target="_blank" href="https://www.linkedin.com/in/sanjay-r-ab6064294/">LinkedIn</a> or <a target="_blank" href="https://www.instagram.com/heheheh_pet/profilecard/?igsh=eXh3MWw4ZzZ3NTRq">Instagram</a>.</p>
<p>Thank you for reading!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Video Subtitle Generator using the Gemini API ]]>
                </title>
                <description>
                    <![CDATA[ In this tutorial, you'll build an AI-powered subtitle generator using Google's Gemini API. We'll create a project called “AI-Subtitle-Generator” using React for the front end and Express for the back end. Get ready for a fun and practical project. Ta... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-video-subtitle-generator-using-the-gemini-api/</link>
                <guid isPermaLink="false">6759af8bb972ec12da4879d3</guid>
                
                    <category>
                        <![CDATA[ gemini ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Express ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Sanjay ]]>
                </dc:creator>
                <pubDate>Wed, 11 Dec 2024 15:28:11 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733638398422/2f468b16-5801-4f8c-bf40-c24d07e219b7.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this tutorial, you'll build an AI-powered subtitle generator using Google's Gemini API. We'll create a project called “AI-Subtitle-Generator” using React for the front end and Express for the back end. Get ready for a fun and practical project.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-how-to-get-your-api-key">How to Get Your API Key</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-front-end-setup">Front End Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-server-setup">Server Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-update-the-front-end">Update the Front End</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-summary">Summary</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>To build this project, you should know the basics of React and Express.</p>
<h2 id="heading-what-is-the-gemini-api">What is the Gemini API?</h2>
<p>Google's Gemini API is a powerful tool that lets you integrate advanced AI capabilities into your applications. Gemini is a multimodal model, which means you can use various types of input, like text, images, audio, and video.</p>
<p>It’s good at analyzing and processing large amounts of text as well as pulling information from videos – which makes it great for our use case of a subtitle generator.</p>
<h2 id="heading-how-to-get-your-api-key">How to Get Your API Key</h2>
<p>An API key acts as a unique identifier and authenticates your requests to the service. It's essential for accessing and using Gemini AI’s capabilities. This key will allow our application to communicate with Gemini and help us build our project.</p>
<p>Go to <a target="_blank" href="https://aistudio.google.com/prompts/new_chat">Google AI Studio</a>, then click “Get API Key”:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733571839232/f5636fd0-c3cd-4c1b-bf7f-5200bce41444.png" alt="Screenshot of Google AI Studio showing the 'Get API Key' button" class="image--center mx-auto" width="1246" height="682" loading="lazy"></p>
<p>After you are redirected to the API KEY page, click “Create API Key“:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1733572045638/c950f7a2-613c-4976-905a-ce5c9dceb901.png" alt="Screenshot showing how to create an API key in Google AI Studio." class="image--center mx-auto" width="1360" height="777" loading="lazy"></p>
<p>A new API KEY will be created. Then make sure you copy the key.</p>
<p>This is your API key. This key is used to authenticate your application's requests to the Gemini API. Each time your application sends a request to Gemini, this key must be included. Gemini uses this key to verify that the request is coming from an authorized source. Without this API key, your requests will be rejected, and you won't be able to access Gemini's services.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Start by creating a new folder for your project. Let's call it <code>ai-subtitle-generator</code>.</p>
<p>Inside the <code>ai-subtitle-generator</code> folder, create two subfolders: <code>client</code> and <code>server</code>. The <code>client</code> folder will contain the React frontend, and the <code>server</code> folder will contain the Express backend.</p>
<h2 id="heading-front-end-setup">Front End Setup</h2>
<p>First, we will focus on the front end and set up a basic React application.</p>
<p>Navigate to the <code>client</code> folder:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> client
</code></pre>
<p>Then create a new React project using Vite. To do that, run the following command:</p>
<pre><code class="lang-bash">npm create vite@latest .
</code></pre>
<p>When prompted, choose “React“. Select “React + TS” or “React + JS”. In this tutorial, I will use React + TS. You can also follow along with JS.</p>
<p>Next, install the dependencies with this command:</p>
<pre><code class="lang-bash">npm install
</code></pre>
<p>Then start the development server:</p>
<pre><code class="lang-bash">npm run dev
</code></pre>
<h4 id="heading-how-to-handle-file-uploads-in-the-frontend">How to Handle File Uploads in the Frontend</h4>
<p>Now in <code>client/src/App.tsx</code>, add the following code:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//  client/src/App.tsx</span>

<span class="hljs-keyword">const</span> App = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> handleSubmit = <span class="hljs-keyword">async</span> (e: React.FormEvent&lt;HTMLFormElement&gt;): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; =&gt; {
    e.preventDefault();
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData(e.currentTarget);
      <span class="hljs-built_in">console</span>.log(formData)
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
    }
  };

  <span class="hljs-keyword">return</span> (
    &lt;div&gt;
      &lt;form onSubmit={handleSubmit}&gt;
        &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span> accept=<span class="hljs-string">"video/*,.mkv"</span> name=<span class="hljs-string">"video"</span> /&gt;
        &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span> /&gt;
      &lt;/form&gt;
    &lt;/div&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<p>In the above code, we have used an input tag that will accept the video and name it as <code>video</code>. This name will be appended to the <code>FormData</code> object.</p>
<p>While sending the video to the server, we need to send it as a key-value pair, where the key is a <code>video</code> and the value is the file data.</p>
<p>Why key-value pairs? Because when the server receives the request, it needs to parse the incoming chunks. After parsing, the video data will be available in <code>req.files[key]</code>, where the <code>key</code> is the name we have assigned in the frontend (<code>video</code> in this case).</p>
<p>This is why we are using the <code>FormData</code> object. When we create a new <code>FormData</code> instance and pass <code>e.target</code> to it, all the form fields and their names will automatically be available as key-value pairs.</p>
<h2 id="heading-server-setup">Server Setup</h2>
<p>Now that we have our API key, let's set up the backend server. This server will handle video uploads from the frontend and communicate with the Gemini API for subtitle generation.</p>
<p>Navigate to <code>server</code> folder:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> server
</code></pre>
<p>And initialize the project:</p>
<pre><code class="lang-bash">npm init -y
</code></pre>
<p>Then install the necessary packages:</p>
<pre><code class="lang-bash">npm install express dotenv cors @google/generative-ai express-fileupload nodemon
</code></pre>
<p>These are the back-end dependencies we’re using in this project:</p>
<ul>
<li><p><code>express</code><strong>:</strong> The web framework for creating the backend API.</p>
</li>
<li><p><code>dotenv</code><strong>:</strong> Loads environment variables from a <code>.env</code> file.</p>
</li>
<li><p><code>cors</code><strong>:</strong> Enables Cross-Origin Resource Sharing, allowing your frontend to communicate with your backend.</p>
</li>
<li><p><code>@google/generative-ai</code><strong>:</strong> The Google AI library for interacting with the Gemini API.</p>
</li>
<li><p><code>express-fileupload</code><strong>:</strong> Handles file uploads, making it easy to access uploaded files on the server.</p>
</li>
<li><p><code>nodemon</code><strong>:</strong> Automatically restarts the server when you make changes to your code.</p>
</li>
</ul>
<h3 id="heading-set-up-the-environment-variables">Set Up the Environment Variables</h3>
<p>Now, create a file called <code>.env</code>. This is where you’ll manage your API keys.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//.env</span>
API_KEY = YOUR_API_API
PORT = <span class="hljs-number">3000</span>
</code></pre>
<h3 id="heading-update-the-packagejson">Update the <code>package.json</code></h3>
<p>For this project, we are using ES6 modules instead of CommonJS. To enable this, update your <code>package.json</code> file with the following code:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"server"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,       <span class="hljs-comment">//Add "type": "module" to enable ES6 modules</span>
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"node server.js"</span>,
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"nodemon server.js"</span>    <span class="hljs-comment">//configure nodemon</span>
  },
  <span class="hljs-attr">"keywords"</span>: [],
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"@google/generative-ai"</span>: <span class="hljs-string">"^0.21.0"</span>,
    <span class="hljs-attr">"cors"</span>: <span class="hljs-string">"^2.8.5"</span>,
    <span class="hljs-attr">"dotenv"</span>: <span class="hljs-string">"^16.4.7"</span>,
    <span class="hljs-attr">"express"</span>: <span class="hljs-string">"^4.21.1"</span>,
    <span class="hljs-attr">"express-fileupload"</span>: <span class="hljs-string">"^1.5.1"</span>,
    <span class="hljs-attr">"nodemon"</span>: <span class="hljs-string">"^3.1.7"</span>
  }
}
</code></pre>
<h3 id="heading-basic-setup-of-express">Basic Setup of Express</h3>
<p>Create a file <code>server.js</code>. Now, let’s set up a basic Express application.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//  server/server.js</span>

<span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">"express"</span>;
<span class="hljs-keyword">import</span> { configDotenv } <span class="hljs-keyword">from</span> <span class="hljs-string">"dotenv"</span>;
<span class="hljs-keyword">import</span> fileUpload <span class="hljs-keyword">from</span> <span class="hljs-string">"express-fileupload"</span>;
<span class="hljs-keyword">import</span> cors <span class="hljs-keyword">from</span> <span class="hljs-string">"cors"</span>

<span class="hljs-keyword">const</span> app = express();

configDotenv();           <span class="hljs-comment">//configure the env</span>
app.use(fileUpload());    <span class="hljs-comment">//it will parse the mutipart data</span>
app.use(express.json());  <span class="hljs-comment">// Enable JSON parsing for request bodies</span>
app.use(cors())           <span class="hljs-comment">//configure cors</span>

app.use(<span class="hljs-string">"/api/subs"</span>,subRoutes);  <span class="hljs-comment">// Use routes for the "/api/subs" endpoint</span>

app.listen(process.env.PORT, <span class="hljs-function">() =&gt;</span> {   <span class="hljs-comment">//access the PORT from the .env</span>
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"server started"</span>);         
});
</code></pre>
<p>In this code, we create an Express app instance and then load our environment variables. This is where we keep sensitive data like API keys secure. Next, we apply middleware functions: <code>fileUpload</code> prepares the server to receive uploaded videos, <code>express.json</code> allows us to receive JSON data, and <code>cors</code> enables communication between our frontend and backend.</p>
<p>We define a route <code>(/api/subs)</code> that will handle all requests related to subtitle generation. The specific logic for these routes will be defined in <code>subs.routes.js</code>. Finally, we start the server, telling it to listen for requests on the port specified in our <code>.env</code> file.</p>
<p>Now we need to create some folders to manage the code. You can also manage the entire code in a single file, but structuring it into separate folders and managing them all that way will be easier.</p>
<p>This is the final folder structure for the server:</p>
<pre><code class="lang-plaintext">server/
├── server.js
├── controller/
│   └── subs.controller.js
├── gemini/
│   ├── gemini.config.js
├── routes/
│   └── subs.routes.js
├── uploads/
├── utils/
│   ├── fileUpload.js
│   └── genContent.js
└── .env
</code></pre>
<p><strong>Note:</strong> Don’t worry about creating this folder structure now. This is just for reference. Follow along with me step by step, and we will build this structure together.</p>
<h3 id="heading-create-the-routes">Create the Routes</h3>
<p>Now create a <code>routes</code> folder and then create <code>subs.routes.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// server/routes/sub.routes.js</span>

<span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">"express"</span>
<span class="hljs-keyword">import</span> { uploadFile } <span class="hljs-keyword">from</span> <span class="hljs-string">"../controller/subs.controller.js"</span>    <span class="hljs-comment">// import the uploadFile function from the controller folder</span>

<span class="hljs-keyword">const</span> router = express.Router()

router.post(<span class="hljs-string">"/"</span>,uploadFile)    <span class="hljs-comment">// define a POST route that calls the uploadFile function</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router     <span class="hljs-comment">// export the router to use in the main server.js file</span>
</code></pre>
<p>This code defines the routes for our server, specifically the route that handles video uploads and subtitle generation.</p>
<p>We create a new router instance using <code>express.Router()</code>. This allows us to define routes separate from our main server file, improving code organization. We define a POST route at the root path <code>("/")</code> of our API endpoint. When a POST request is made to this route (which will happen when a user submits the video upload form on the frontend), the <code>uploadFile</code> function is called. This function will handle the actual upload and subtitle generation.</p>
<p>Finally, we export the router so that it can be used in our main server file <code>(server.js)</code> to connect this route to the main application.</p>
<h3 id="heading-configure-gemini">Configure Gemini</h3>
<p>Now, let's configure how our application will interact with Gemini.</p>
<p>Create a <code>gemini</code> folder and then create a new file called <code>gemini.config.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//  server/gemini/gemini.config.js</span>

<span class="hljs-keyword">import</span> {
  GoogleGenerativeAI,
  HarmBlockThreshold,
  HarmCategory,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@google/generative-ai"</span>;
<span class="hljs-keyword">import</span> { configDotenv } <span class="hljs-keyword">from</span> <span class="hljs-string">"dotenv"</span>;
configDotenv();

<span class="hljs-keyword">const</span> genAI = <span class="hljs-keyword">new</span> GoogleGenerativeAI(process.env.API_KEY);  <span class="hljs-comment">// Initialize Google Generative AI with the API key</span>

<span class="hljs-keyword">const</span> safetySettings = [
  {
    <span class="hljs-attr">category</span>: HarmCategory.HARM_CATEGORY_HARASSMENT,
    <span class="hljs-attr">threshold</span>: HarmBlockThreshold.BLOCK_NONE,
  },
  {
    <span class="hljs-attr">category</span>: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
    <span class="hljs-attr">threshold</span>: HarmBlockThreshold.BLOCK_NONE,
  },
  {
    <span class="hljs-attr">category</span>: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
    <span class="hljs-attr">threshold</span>: HarmBlockThreshold.BLOCK_NONE,
  },
  {
    <span class="hljs-attr">category</span>: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
    <span class="hljs-attr">threshold</span>: HarmBlockThreshold.BLOCK_NONE,
  },
];

<span class="hljs-keyword">const</span> model = genAI.getGenerativeModel({
  <span class="hljs-attr">model</span>: <span class="hljs-string">"gemini-1.5-flash-001"</span>,    <span class="hljs-comment">//choose the model</span>
  <span class="hljs-attr">safetySettings</span>: safetySettings,   <span class="hljs-comment">//optional safety settings</span>
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> model;    <span class="hljs-comment">//export the model</span>
</code></pre>
<p>In the code above, the <code>safetySettings</code> are optional. These settings allow you to define thresholds for potentially harmful content (like hate speech, violence, or explicit material) in Gemini's output.</p>
<p>You can read more about Gemini’s safety settings <a target="_blank" href="https://ai.google.dev/gemini-api/docs/safety-settings">here</a>.</p>
<h3 id="heading-create-a-controller-to-handle-endpoint-logic">Create a Controller to Handle Endpoint Logic</h3>
<p>Now, create a <code>controller</code> folder, and inside it create a file named <code>subs.controller.js</code>. In this file, you'll handle the endpoint logic for interacting with the Gemini model.</p>
<p>In <code>server/controller/subs.controller.js</code>, add this code:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// server/controller/subs.controller.js</span>

<span class="hljs-keyword">import</span> { fileURLToPath } <span class="hljs-keyword">from</span> <span class="hljs-string">"url"</span>;
<span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">"path"</span>;
<span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;

<span class="hljs-keyword">const</span> __filename = fileURLToPath(<span class="hljs-keyword">import</span>.meta.url);  <span class="hljs-comment">//converts the module URL to a file path</span>
<span class="hljs-keyword">const</span> __dirname = path.dirname(__filename);   <span class="hljs-comment">//get the current file directory</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> uploadFile = <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">if</span> (!req.files || !req.files.video) {   <span class="hljs-comment">//if there is no file available, return error to the client</span>
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">"No video uploaded"</span> });
    }

    <span class="hljs-keyword">const</span> videoFile = req.files.video;   <span class="hljs-comment">//access the video</span>
    <span class="hljs-keyword">const</span> uploadDir = path.join(__dirname, <span class="hljs-string">".."</span>, <span class="hljs-string">"uploads"</span>);   <span class="hljs-comment">//path to upload the video temporarily</span>

    <span class="hljs-keyword">if</span> (!fs.existsSync(uploadDir)) {   <span class="hljs-comment">//check if the directory exists</span>
      fs.mkdirSync(uploadDir);      <span class="hljs-comment">//if not create a new one</span>
    }

    <span class="hljs-keyword">const</span> uploadPath = path.join(uploadDir, videoFile.name);  

    <span class="hljs-keyword">await</span> videoFile.mv(uploadPath);  <span class="hljs-comment">//it moves the video from the buffer to the "upload" folder</span>

    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).json({ <span class="hljs-attr">message</span>:<span class="hljs-string">"file uploaded sucessfully"</span> });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> res
      .status(<span class="hljs-number">500</span>)
      .json({ <span class="hljs-attr">error</span>: <span class="hljs-string">"Internal server error: "</span> + error.message });
  }
};
</code></pre>
<p>Since we are using an ES6 module, the <code>__dirname</code> is not available by default. The file handling mechanism is different compared to CommonJS. Because of this, we’ll use <code>fileURLToPath</code> to handle file paths.</p>
<p>We moved the file from the default temporary location which is the buffer to the <code>uploads</code> folder.</p>
<p>But the file upload process is not yet complete. We still need to send the file to Google AI File Manager, and after uploading, it will return a URI. This URI will then be passed to the model for video analysis.</p>
<h3 id="heading-how-to-upload-a-file-to-the-google-ai-file-manager">How to Upload a File to the Google AI File Manager</h3>
<p>Create a folder <code>utils</code> and create a file <code>fileUpload.js</code>. You can refer to the folder structure provided above.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//  server/utils/fileUpload.js</span>

<span class="hljs-keyword">import</span> { GoogleAIFileManager, FileState } <span class="hljs-keyword">from</span> <span class="hljs-string">"@google/generative-ai/server"</span>;
<span class="hljs-keyword">import</span> { configDotenv } <span class="hljs-keyword">from</span> <span class="hljs-string">"dotenv"</span>;
configDotenv();

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> fileManager = <span class="hljs-keyword">new</span> GoogleAIFileManager(process.env.API_KEY);  <span class="hljs-comment">//create a new GoogleAIFileManager instance</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fileUpload</span>(<span class="hljs-params">path, videoData</span>) </span>{  
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> uploadResponse = <span class="hljs-keyword">await</span> fileManager.uploadFile(path, {   <span class="hljs-comment">//give the path as an argument</span>
      <span class="hljs-attr">mimeType</span>: videoData.mimetype,  
      <span class="hljs-attr">displayName</span>: videoData.name,
    });
    <span class="hljs-keyword">const</span> name = uploadResponse.file.name;
    <span class="hljs-keyword">let</span> file = <span class="hljs-keyword">await</span> fileManager.getFile(name);    
    <span class="hljs-keyword">while</span> (file.state === FileState.PROCESSING) {     <span class="hljs-comment">//check the state of the file</span>
      process.stdout.write(<span class="hljs-string">"."</span>);
      <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">res</span>) =&gt;</span> <span class="hljs-built_in">setTimeout</span>(res, <span class="hljs-number">10000</span>));   <span class="hljs-comment">//check every 10 second</span>
      file = <span class="hljs-keyword">await</span> fileManager.getFile(name);
    }
    <span class="hljs-keyword">if</span> (file.state === FileState.FAILED) {   
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Video processing failed"</span>);
    }
    <span class="hljs-keyword">return</span> file;   <span class="hljs-comment">// return the file object, containing the upload file information and the uri</span>
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">throw</span> error;
  }
}
</code></pre>
<p>In the code above, we created a function called <code>fileUpload</code> that takes two arguments. These arguments will be passed from the controller function, which we'll set up later.</p>
<p>The <code>fileUpload</code> function uses the <code>fileManager.uploadFile</code> method to send the video to Google's servers. This method needs two arguments: the file path and an object containing metadata about the file (its MIME type and display name).</p>
<p>Because video processing on Google's servers takes time, we need to check the file's status. We do this using a loop that checks the file's state every 10 seconds using <code>fileManager.getFile()</code>. The loop continues as long as the file's state is <code>PROCESSING</code>. Once the state changes to either <code>SUCCESS</code> or <code>FAILED</code>, the loop stops.</p>
<p>The function then checks if the processing was successful. If so, it returns the file object, which contains information about the uploaded and processed video, including its URI. Otherwise, if the state is <code>FAILED</code>, the function throws an error.</p>
<h3 id="heading-pass-the-uri-to-the-gemini-model">Pass the URI to the Gemini Model</h3>
<p>Now in the <code>utils</code> folder, create a file called <code>genContent.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// server/utils/genContent.js</span>

<span class="hljs-keyword">import</span> model <span class="hljs-keyword">from</span> <span class="hljs-string">"../gemini/gemini.config.js"</span>;
<span class="hljs-keyword">import</span> { configDotenv } <span class="hljs-keyword">from</span> <span class="hljs-string">"dotenv"</span>;
configDotenv();

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getContent</span>(<span class="hljs-params">file</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> model.generateContent([
      {
        <span class="hljs-attr">fileData</span>: {
          <span class="hljs-attr">mimeType</span>: file.mimeType,
          <span class="hljs-attr">fileUri</span>: file.uri,
        },
      },
      {
        <span class="hljs-attr">text</span>: <span class="hljs-string">"You need to write a subtitle for this full video, write the subtitle in the SRT format, don't write anything else other than a subtitle in the response, create accurate subtitle."</span>,
      },
    ]);
    <span class="hljs-keyword">return</span> result.response.text();
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">throw</span> error;
  }
}
</code></pre>
<p>Import the model that we configured earlier. Create a function called <code>getContent</code>. The <code>getContent</code> function takes the file object (returned from the <code>fileUpload</code> function).</p>
<p>Pass the file URI and the <code>mimi</code> to the model. Then we’ll provide a prompt instructing the model to generate subtitles for the entire video in SRT format. You can also add your prompt if you want. Then return the response.</p>
<h3 id="heading-update-the-subscontrollerjs-file">Update the <code>subs.controller.js</code> File</h3>
<p>Finally, we need to update the controller file. We've created the <code>fileUpload</code> and <code>getContent</code> functions, and now we'll use them in the controller and provide the required arguments.</p>
<p>In the <code>server/controller/subs.controller.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//  server/controller/subs.controller.js</span>

<span class="hljs-keyword">import</span> { fileURLToPath } <span class="hljs-keyword">from</span> <span class="hljs-string">"url"</span>;
<span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">"path"</span>;
<span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">"fs"</span>;
<span class="hljs-keyword">import</span> { fileUpload } <span class="hljs-keyword">from</span> <span class="hljs-string">"../utils/fileUpload.js"</span>;
<span class="hljs-keyword">import</span> { getContent } <span class="hljs-keyword">from</span> <span class="hljs-string">"../utils/genContent.js"</span>;

<span class="hljs-keyword">const</span> __filename = fileURLToPath(<span class="hljs-keyword">import</span>.meta.url);
<span class="hljs-keyword">const</span> __dirname = path.dirname(__filename);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> uploadFile = <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">if</span> (!req.files || !req.files.video) {
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">"No video uploaded"</span> });
    }

    <span class="hljs-keyword">const</span> videoFile = req.files.video;
    <span class="hljs-keyword">const</span> uploadDir = path.join(__dirname, <span class="hljs-string">".."</span>, <span class="hljs-string">"uploads"</span>);

    <span class="hljs-keyword">if</span> (!fs.existsSync(uploadDir)) {
      fs.mkdirSync(uploadDir);
    }

    <span class="hljs-keyword">const</span> uploadPath = path.join(uploadDir, videoFile.name);

    <span class="hljs-keyword">await</span> videoFile.mv(uploadPath);

    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fileUpload(uploadPath, req.files.video);  <span class="hljs-comment">//we pass 'uploadPath' and the video file data to 'fileUpload'</span>
    <span class="hljs-keyword">const</span> genContent = <span class="hljs-keyword">await</span> getContent(response);   <span class="hljs-comment">//the 'response' (containing the file URI) is passed to 'getContent'</span>

    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).json({ <span class="hljs-attr">subs</span>: genContent });   <span class="hljs-comment">//// return the generated subtitles to the client</span>
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error uploading video:"</span>, error);
    <span class="hljs-keyword">return</span> res
      .status(<span class="hljs-number">500</span>)
      .json({ <span class="hljs-attr">error</span>: <span class="hljs-string">"Internal server error: "</span> + error.message });
  }
};
</code></pre>
<p>With this, the backend API is complete. Now, we'll move on to updating the front end.</p>
<h2 id="heading-update-the-front-end">Update the Front End</h2>
<p>Our frontend currently only allows users to select a video. In this section, we'll update it to send the video data to our backend for processing. The frontend will then receive the generated subtitles from the backend and initiate a download of the <code>.srt</code> file.</p>
<p>Navigate to the <code>client</code> folder:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> client
</code></pre>
<p>Install <code>axios</code>. We’ll use it to handle HTTP requests.</p>
<pre><code class="lang-bash">npm install axios
</code></pre>
<p>In the <code>client/src/App.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//   client/src/App.tsx</span>

<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">"axios"</span>;

<span class="hljs-keyword">const</span> App = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> handleSubmit = <span class="hljs-keyword">async</span> (e: React.FormEvent&lt;HTMLFormElement&gt;): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; =&gt; {
    e.preventDefault();
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData(e.currentTarget);
      <span class="hljs-comment">// sending a POST request with form data</span>
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> axios.post(
        <span class="hljs-string">"http://localhost:3000/api/subs/"</span>,   
        formData
      );
<span class="hljs-comment">// creating a Blob from the server response and triggering the file download</span>
      <span class="hljs-keyword">const</span> blob = <span class="hljs-keyword">new</span> Blob([response.data.subs], { <span class="hljs-keyword">type</span>: <span class="hljs-string">"text/plain"</span> }); 
      <span class="hljs-keyword">const</span> link = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"a"</span>);
      link.href = URL.createObjectURL(blob);
      link.download = <span class="hljs-string">"subtitle.srt"</span>;
      link.click();
      link.remove();
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(error);
    }
  };

  <span class="hljs-keyword">return</span> (
    &lt;div&gt;
      &lt;form onSubmit={handleSubmit}&gt;
        &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span> accept=<span class="hljs-string">"video/*,.mkv"</span> name=<span class="hljs-string">"video"</span> /&gt;
        &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span> /&gt;
      &lt;/form&gt;
    &lt;/div&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<p><code>axios</code> makes the POST request to your backend API endpoint <code>(/api/subs)</code>. The server will process the video, and this might take some time.</p>
<p>After the server sends the generated subtitles, the frontend receives them as a response. To handle this response and allow users to download the subtitles, we'll use a Blob. A Blob (Binary Large Object) is a web API object that represents raw binary data, essentially acting like a file. In our case, the subtitles returned from the server will be converted into a Blob, which will then allow us to trigger a download in the user's browser.</p>
<h2 id="heading-summary">Summary</h2>
<p>In this tutorial, you learned how to build an AI-powered subtitle generator using Google's Gemini API, React, and Express. You can upload videos, send them to the Gemini API for subtitle generation, and provide the generated subtitles for download.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>That's it! You've successfully built an AI-powered subtitle generator using the Gemini API. For quicker testing, start with shorter video clips (3-5 minutes). Longer videos might take more time to process.</p>
<p>Want to create a customizable video prompting application? Just add an input field to let users enter their prompts, send that prompt to the server, and use it in place of the hardcoded prompt. That's all it takes.</p>
<p>For more information about the Gemini API, refer to the official <a target="_blank" href="https://ai.google.dev/gemini-api/docs#node.js">Gemini API Docs</a></p>
<p>You can find the full code here: <a target="_blank" href="https://github.com/sanjayr-12/ai-subtitle-generator">AI-Subtitle-Generator</a></p>
<p>If there are any mistakes or you have any questions, contact me on <a target="_blank" href="https://www.linkedin.com/in/sanjay-r-ab6064294/">LinkedIn</a> or <a target="_blank" href="https://www.instagram.com/heheheh_pet/profilecard/?igsh=eXh3MWw4ZzZ3NTRq">Instagram</a>.</p>
<p>Thank you for reading!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a CRUD Application using React and Convex ]]>
                </title>
                <description>
                    <![CDATA[ CRUD operations are the basis of every application, so it is essential to become proficient in them when learning new technologies. In this tutorial, you’ll learn how to build a CRUD application using React and Convex. We’ll cover these operations by... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-crud-app-react-and-convex/</link>
                <guid isPermaLink="false">671a5f1685689d5790d302c5</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ crud ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Sanjay ]]>
                </dc:creator>
                <pubDate>Thu, 24 Oct 2024 14:52:06 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729397399755/9c747607-fa82-4caf-9c20-8e64ec82c3f2.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>CRUD operations are the basis of every application, so it is essential to become proficient in them when learning new technologies.</p>
<p>In this tutorial, you’ll learn how to build a CRUD application using React and Convex. We’ll cover these operations by building a project called Book Collections. In this project, users will be able to add books and update their status once they complete a book.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-convex">What is Convex?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-your-project">How to Set Up Your Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-the-schema">How to Create the Schema</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-the-ui">How to Create the UI</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-crud-functions">How to Create CRUD Functions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-styling">Styling</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-summary">Summary</a></p>
</li>
</ul>
<h2 id="heading-what-is-convex">What is Convex?</h2>
<p>Convex is the Baas Platform that simplifies backend development. Convex comes with a real-time database, and you do not need to worry about writing server-side logic separately because it provides methods for querying and mutating the database.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>In order to follow this tutorial, you must know the fundamentals of React. I will be using TypeScript in this project, but it is optional, so you can also follow along with JavaScript.</p>
<h2 id="heading-how-to-set-up-your-project">How to Set Up Your Project</h2>
<p>Create a separate folder for the project and name it as you wish – I will name mine <strong>Books.</strong> We’ll set up Convex and React in that folder.</p>
<p>You can create a React app using this command:</p>
<pre><code class="lang-bash">npm create vite@latest my-app -- --template react-ts
</code></pre>
<p>If you want to work with JavaScript, then remove the <code>ts</code> at the end. That is:</p>
<pre><code class="lang-bash">npm create vite@latest my-app -- --template react
</code></pre>
<h3 id="heading-how-to-setup-convex">How to Setup Convex</h3>
<p>We have to install Convex in the same folder. You can do that using this command:</p>
<pre><code class="lang-bash">npm install convex
</code></pre>
<p>Next, run <code>npx convex dev</code>. If you’re doing this for the first time, it should ask you for authentication. Otherwise, it should ask for the project name.</p>
<p>You can visit the <a target="_blank" href="https://www.convex.dev/">Convex dashboard</a> to see the project that you have created.</p>
<p>Now that we have the Convex and React App set up, we need to connect the Convex backend to the React app.</p>
<p>In the <strong>src/main.tsx</strong>, wrap your <code>App</code> component with the <code>ConvexReactClient</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createRoot } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-dom/client"</span>;
<span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">"./App.tsx"</span>;
<span class="hljs-keyword">import</span> { ConvexProvider, ConvexReactClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"convex/react"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"./index.css"</span>

<span class="hljs-keyword">const</span> convex = <span class="hljs-keyword">new</span> ConvexReactClient(<span class="hljs-keyword">import</span>.meta.env.VITE_CONVEX_URL <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>);

createRoot(<span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"root"</span>)!).render(
  &lt;ConvexProvider client={convex}&gt;
    &lt;App /&gt;
  &lt;/ConvexProvider&gt;
);
</code></pre>
<p>When you set up Convex, a <code>.env.local</code> was created. You can see your backend URL in that file.</p>
<p>In the line below, we instantiated the React Convex Client with the URL.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> convex = <span class="hljs-keyword">new</span> ConvexReactClient(<span class="hljs-keyword">import</span>.meta.env.VITE_CONVEX_URL <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>);
</code></pre>
<h2 id="heading-how-to-create-the-schema">How to Create the Schema</h2>
<p>In your main project directory, you should see the <strong>convex</strong> directory. We’ll handle the database queries and mutations here.</p>
<p>Create a <strong>schema.ts</strong> file in the <strong>convex</strong> folder:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { defineSchema, defineTable } <span class="hljs-keyword">from</span> <span class="hljs-string">"convex/server"</span>;
<span class="hljs-keyword">import</span> { v } <span class="hljs-keyword">from</span> <span class="hljs-string">"convex/values"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineSchema({
  books: defineTable({
    title: v.string(),
    author: v.string(),
    isCompleted: v.boolean(),
  }),
});
</code></pre>
<p>You can define a Schema for your document with <code>defineSchema</code> and create a table with <code>defineTable</code>. Convex provides these functions for defining a schema and creating a table.</p>
<p><code>v</code> is the type validator, it is used to provide types for each data we add to the table.</p>
<p>For this project, as it is a book collection application, the structure will have <code>title</code>, <code>author</code>, and <code>isCompleted</code>. You can add more fields.</p>
<p>Now that you have defined your schema, let’s set up the basic UI in React.</p>
<h2 id="heading-how-to-create-the-ui">How to Create the UI</h2>
<p>In the <strong>src</strong> folder, create a folder called <strong>component</strong> and a file <strong>Home.tsx</strong>. Here, you can define the UI.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"../styles/home.css"</span>;

<span class="hljs-keyword">const</span> Home = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> [title, setTitle] = useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> [author, setAuthor] = useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"main-container"</span>&gt;
      &lt;h1&gt;Book Collections&lt;/h1&gt;
      &lt;form onSubmit={handleSubmit}&gt;
        &lt;input
          <span class="hljs-keyword">type</span>=<span class="hljs-string">"text"</span>
          name=<span class="hljs-string">"title"</span>
          value={title}
          onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setTitle(e.target.value)}
          placeholder=<span class="hljs-string">"book title"</span>
        /&gt;
        &lt;br /&gt;
        &lt;input
          <span class="hljs-keyword">type</span>=<span class="hljs-string">"text"</span>
          name=<span class="hljs-string">"author"</span>
          value={author}
          onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setAuthor(e.target.value)}
          placeholder=<span class="hljs-string">"book author"</span>
        /&gt;
        &lt;br /&gt;
        &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span> /&gt;
      &lt;/form&gt;
      {books ? &lt;Books books={books} /&gt; : <span class="hljs-string">"Loading..."</span>}
    &lt;/div&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Home;
</code></pre>
<p>You can create your component as you wish. I added two input fields <code>title</code>, <code>author</code>, and <code>submit</code> button. This is the basic structure. Now we can create CRUD methods in the backend.</p>
<h2 id="heading-how-to-create-crud-functions">How to Create CRUD Functions</h2>
<p>In the <strong>convex</strong> folder, you can create a separate <strong>queries.ts</strong> file for the CRUD functions.</p>
<h3 id="heading-create-function">Create Function</h3>
<p>In <strong>convex/queries.ts</strong>:</p>
<p>You can define a function <code>createBooks</code>. You can use the <code>mutation</code> function from Convex to create, update, and delete data. Reading the data will come under <code>query</code>.</p>
<p>The <code>mutation</code> function expects these arguments:</p>
<ul>
<li><p><code>agrs</code>: the data we need to store in the database.</p>
</li>
<li><p><code>handler</code>: handles the logic to store date in the database. The <code>handler</code> is an async function, and it has two arguments: <code>ctx</code> and <code>args</code>. Here, <code>ctx</code> is the context object that we’ll use to handle the database operations.</p>
</li>
</ul>
<p>You’ll use the <code>insert</code> method to insert new data. The first parameter in the <code>insert</code> is the table name and the second is the data that needs to be inserted.</p>
<p>Lastly, you can return the data from the database.</p>
<p>Here’s the code:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { mutation} <span class="hljs-keyword">from</span> <span class="hljs-string">"./_generated/server"</span>;
<span class="hljs-keyword">import</span> { v } <span class="hljs-keyword">from</span> <span class="hljs-string">"convex/values"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createBooks = mutation({
  args: { title: v.string(), author: v.string() },
  handler: <span class="hljs-keyword">async</span> (ctx, args) =&gt; {
    <span class="hljs-keyword">const</span> newBookId = <span class="hljs-keyword">await</span> ctx.db.insert(<span class="hljs-string">"books"</span>, {
      title: args.title,
      author: args.author,
      isCompleted: <span class="hljs-literal">false</span>,
    });
    <span class="hljs-keyword">return</span> newBookId;
  },
});
</code></pre>
<h3 id="heading-read-function">Read Function</h3>
<p>In <strong>convex/queries.ts</strong>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { query } <span class="hljs-keyword">from</span> <span class="hljs-string">"./_generated/server"</span>;
<span class="hljs-keyword">import</span> { v } <span class="hljs-keyword">from</span> <span class="hljs-string">"convex/values"</span>;

<span class="hljs-comment">//read</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getBooks = query({
  args: {},
  handler: <span class="hljs-keyword">async</span> (ctx) =&gt; {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> ctx.db.query(<span class="hljs-string">"books"</span>).collect();
  },
});
</code></pre>
<p>In this read operation, we used the built-in <code>query</code> function from Convex. Here, <code>args</code> will be empty since we are not getting any data from the user. Similarly, the <code>handler</code> function is async and uses the <code>ctx</code> object to query the database and return the data.</p>
<h3 id="heading-update-function">Update Function</h3>
<p>In <strong>convex/queries.ts</strong>:</p>
<p>Create a <code>updateStatus</code> function. We are only going to update the <code>isCompleted</code> status.</p>
<p>Here, you need to get the document ID and the status from the user. In the <code>args</code>, we’ll define <code>id</code> and the <code>isCompleted</code>, which will come from the user.</p>
<p>In the <code>handler</code>, we’ll use the <code>patch</code> method to update the data. The <code>patch</code> method expects two arguments: the first argument is the <code>id</code> of the document and the second is the updated data.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { mutation } <span class="hljs-keyword">from</span> <span class="hljs-string">"./_generated/server"</span>;
<span class="hljs-keyword">import</span> { v } <span class="hljs-keyword">from</span> <span class="hljs-string">"convex/values"</span>;

<span class="hljs-comment">//update</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> updateStatus = mutation({
  args: { id: v.id(<span class="hljs-string">"books"</span>), isCompleted: v.boolean() },
  handler: <span class="hljs-keyword">async</span> (ctx, args) =&gt; {
    <span class="hljs-keyword">const</span> { id } = args;
    <span class="hljs-keyword">await</span> ctx.db.patch(id, { isCompleted: args.isCompleted });
    <span class="hljs-keyword">return</span> <span class="hljs-string">"updated"</span>
  },
});
</code></pre>
<h3 id="heading-delete-function">Delete Function</h3>
<p>In <strong>convex/queries.ts</strong>:</p>
<p>Create a <code>deleteBooks</code> function and use the <code>mutation</code> function. We’ll need the ID of the document to be deleted. In the <code>args</code>, define an ID. In the <code>handler</code>, use the <code>ctx</code> object <code>delete</code> method, and pass the ID. This will delete the document.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { mutation } <span class="hljs-keyword">from</span> <span class="hljs-string">"./_generated/server"</span>;
<span class="hljs-keyword">import</span> { v } <span class="hljs-keyword">from</span> <span class="hljs-string">"convex/values"</span>;

<span class="hljs-comment">//delete</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> deleteBooks = mutation({
  args: { id: v.id(<span class="hljs-string">"books"</span>) },
  handler: <span class="hljs-keyword">async</span> (ctx, args) =&gt; {
    <span class="hljs-keyword">await</span> ctx.db.delete(args.id);
    <span class="hljs-keyword">return</span> <span class="hljs-string">"deleted"</span>;
  },
});
</code></pre>
<p>As of now, you have completed the CRUD functions in the backend. Now we need to make it work in the UI. Let’s jump back to React.</p>
<h3 id="heading-update-the-ui">Update the UI</h3>
<p>You’ve already created some basic UI in the React app, along with some input fields. Let’s update it.</p>
<p>In <strong>src/component/Home.tsx</strong>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useQuery, useMutation } <span class="hljs-keyword">from</span> <span class="hljs-string">"convex/react"</span>;
<span class="hljs-keyword">import</span> { api } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../convex/_generated/api"</span>;
<span class="hljs-keyword">import</span> { Books } <span class="hljs-keyword">from</span> <span class="hljs-string">"./Books"</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"../styles/home.css"</span>;

<span class="hljs-keyword">const</span> Home = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> [title, setTitle] = useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> [author, setAuthor] = useState(<span class="hljs-string">""</span>);
  <span class="hljs-keyword">const</span> books = useQuery(api.queries.getBooks);
  <span class="hljs-keyword">const</span> createBooks = useMutation(api.queries.createBooks);

  <span class="hljs-keyword">const</span> handleSubmit = (e: React.FormEvent&lt;HTMLFormElement&gt;): <span class="hljs-function"><span class="hljs-params">void</span> =&gt;</span> {
    e.preventDefault();
    createBooks({ title, author })
      .then(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"created"</span>);
        setTitle(<span class="hljs-string">""</span>);
        setAuthor(<span class="hljs-string">""</span>);
      })
      .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(err));
  };
  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"main-container"</span>&gt;
      &lt;h1&gt;Book Collections&lt;/h1&gt;
      &lt;form onSubmit={handleSubmit}&gt;
        &lt;input
          <span class="hljs-keyword">type</span>=<span class="hljs-string">"text"</span>
          name=<span class="hljs-string">"title"</span>
          value={title}
          onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setTitle(e.target.value)}
          placeholder=<span class="hljs-string">"book title"</span>
        /&gt;
        &lt;br /&gt;
        &lt;input
          <span class="hljs-keyword">type</span>=<span class="hljs-string">"text"</span>
          name=<span class="hljs-string">"author"</span>
          value={author}
          onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setAuthor(e.target.value)}
          placeholder=<span class="hljs-string">"book author"</span>
        /&gt;
        &lt;br /&gt;
        &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span> /&gt;
      &lt;/form&gt;
      {books ? &lt;Books books={books} /&gt; : <span class="hljs-string">"Loading..."</span>}
    &lt;/div&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Home;
</code></pre>
<p>We can now use the backend API functions by using <code>api</code> from Convex. As you can see, we called two API functions: you can use <code>useQuery</code> if you’re going to read data and <code>useMutation</code> if you want to change data. Now in this file, we are doing, two operations that are create and read.</p>
<p>We got all the data by using this method:</p>
<pre><code class="lang-javascript"> <span class="hljs-keyword">const</span> books = useQuery(api.queries.getBooks);
</code></pre>
<p>The array of objects will be stored in the books variable.</p>
<p>We got the create function from the backend with this line of code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> createBooks = useMutation(api.queries.createBooks);
</code></pre>
<h3 id="heading-how-to-use-the-create-function-in-the-ui">How to Use the Create Function in the UI</h3>
<p>Let’s use the create function in the UI.</p>
<p>Since input fields are in the <code>form</code> tag, we’ll use the <code>onSubmit</code> attribute to handle the form submission.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//In the Home.tsx</span>

<span class="hljs-keyword">const</span> handleSubmit = (e: React.FormEvent&lt;HTMLFormElement&gt;): <span class="hljs-function"><span class="hljs-params">void</span> =&gt;</span> {
    e.preventDefault();
    createBooks({ title, author })
      .then(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"created"</span>);
        setTitle(<span class="hljs-string">""</span>);
        setAuthor(<span class="hljs-string">""</span>);
      })
      .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(err));
  };
</code></pre>
<p>When you click submit, it triggers the <code>handleSubmit</code> function.</p>
<p>We used the <code>createBooks</code> to pass the <code>title</code> and <code>author</code> from the state. The endpoint function is async, so we can use the <code>handleSubmit</code> as async or use <code>.then</code>. I used the <code>.then</code> method to handle the asynchronous data.</p>
<p>You can create a separate component to display the data fetched from the database. The returned data is in the <strong>Home.tsx</strong>, so we will pass the data to the <strong>Books.tsx</strong> component as props.</p>
<p>In <strong>Books.tsx</strong>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { book } <span class="hljs-keyword">from</span> <span class="hljs-string">"../types/book.type"</span>;
<span class="hljs-keyword">import</span> { useMutation } <span class="hljs-keyword">from</span> <span class="hljs-string">"convex/react"</span>;
<span class="hljs-keyword">import</span> { api } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../convex/_generated/api"</span>;
<span class="hljs-keyword">import</span> { Id } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../convex/_generated/dataModel"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"../styles/book.css"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Books = <span class="hljs-function">(<span class="hljs-params">{ books }: { books: book[] }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> [update, setUpdate] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [id, setId] = useState(<span class="hljs-string">""</span>);

  <span class="hljs-keyword">const</span> deleteBooks = useMutation(api.queries.deleteBooks);
  <span class="hljs-keyword">const</span> updateStatus = useMutation(api.queries.updateStatus);

  <span class="hljs-keyword">const</span> handleClick = <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    setId(id);
    setUpdate(!update);
  };

  <span class="hljs-keyword">const</span> handleDelete = <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    deleteBooks({ id: id <span class="hljs-keyword">as</span> Id&lt;<span class="hljs-string">"books"</span>&gt; })
      .then(<span class="hljs-function">(<span class="hljs-params">mess</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(mess))
      .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(err));
  };

  <span class="hljs-keyword">const</span> handleUpdate = <span class="hljs-function">(<span class="hljs-params">e: React.FormEvent&lt;HTMLFormElement&gt;, id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    e.preventDefault();
    <span class="hljs-keyword">const</span> formdata = <span class="hljs-keyword">new</span> FormData(e.currentTarget);
    <span class="hljs-keyword">const</span> isCompleted: <span class="hljs-built_in">boolean</span> =
      (formdata.get(<span class="hljs-string">"completed"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>) === <span class="hljs-string">"true"</span>;
    updateStatus({ id: id <span class="hljs-keyword">as</span> Id&lt;<span class="hljs-string">"books"</span>&gt;, isCompleted })
      .then(<span class="hljs-function">(<span class="hljs-params">mess</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(mess))
      .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(err));
    setUpdate(<span class="hljs-literal">false</span>);
  };

  <span class="hljs-keyword">return</span> (
    &lt;div&gt;
      {books.map(<span class="hljs-function">(<span class="hljs-params">data: book, index: <span class="hljs-built_in">number</span></span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> (
          &lt;div
            key={data._id}
            className={<span class="hljs-string">`book-container <span class="hljs-subst">${data.isCompleted ? <span class="hljs-string">"completed"</span> : <span class="hljs-string">"not-completed"</span>}</span>`</span>}
          &gt;
            &lt;h3&gt;Book no: {index + <span class="hljs-number">1</span>}&lt;/h3&gt;
            &lt;p&gt;Book title: {data.title}&lt;/p&gt;
            &lt;p&gt;Book Author: {data.author}&lt;/p&gt;
            &lt;p&gt;
              Completed Status:{<span class="hljs-string">" "</span>}
              {data.isCompleted ? <span class="hljs-string">"Completed"</span> : <span class="hljs-string">"Not Completed"</span>}
            &lt;/p&gt;
            &lt;button onClick={<span class="hljs-function">() =&gt;</span> handleClick(data._id)}&gt;Update&lt;/button&gt;
            {id === data._id &amp;&amp; update &amp;&amp; (
              &lt;&gt;
                &lt;form onSubmit={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> handleUpdate(e, data._id)}&gt;
                  &lt;select name=<span class="hljs-string">"completed"</span>&gt;
                    &lt;option value=<span class="hljs-string">"true"</span>&gt;Completed&lt;/option&gt;
                    &lt;option value=<span class="hljs-string">"false"</span>&gt;Not Completed&lt;/option&gt;
                  &lt;/select&gt;
                  &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span> /&gt;
                &lt;/form&gt;
              &lt;/&gt;
            )}
            &lt;button onClick={<span class="hljs-function">() =&gt;</span> handleDelete(data._id)}&gt;<span class="hljs-keyword">delete</span>&lt;/button&gt;
          &lt;/div&gt;
        );
      })}
    &lt;/div&gt;
  );
};
</code></pre>
<p>In the <strong>Books.jsx</strong> component, you can display data from the database and handle the functionality for updating and deleting records.</p>
<p>Let’s walk through each of these features step by step.</p>
<h3 id="heading-how-to-display-the-data">How to Display the Data</h3>
<p>You can get the data passed as a prop in the <code>Home.tsx</code> component. If you are using TypeScript, I have defined a type for the object that is returned from the query. You can ignore this if you are using JavaScript.</p>
<p>Create `<strong>books.types.ts</strong>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> book = {
    _id: <span class="hljs-built_in">string</span>,
    title: <span class="hljs-built_in">string</span>,
    author: <span class="hljs-built_in">string</span>,
    isCompleted: <span class="hljs-built_in">boolean</span>
}
</code></pre>
<p>You can use the <code>map</code> function to display the data.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { book } <span class="hljs-keyword">from</span> <span class="hljs-string">"../types/book.type"</span>;
<span class="hljs-keyword">import</span> { useMutation } <span class="hljs-keyword">from</span> <span class="hljs-string">"convex/react"</span>;
<span class="hljs-keyword">import</span> { api } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../convex/_generated/api"</span>;
<span class="hljs-keyword">import</span> { Id } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../convex/_generated/dataModel"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"../styles/book.css"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Books = <span class="hljs-function">(<span class="hljs-params">{ books }: { books: book[] }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> [update, setUpdate] = useState(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">return</span> (
    &lt;div&gt;
      {books.map(<span class="hljs-function">(<span class="hljs-params">data: book, index: <span class="hljs-built_in">number</span></span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> (
          &lt;div
            key={data._id}
            className={<span class="hljs-string">`book-container <span class="hljs-subst">${data.isCompleted ? <span class="hljs-string">"completed"</span> : <span class="hljs-string">"not-completed"</span>}</span>`</span>}
          &gt;
            &lt;h3&gt;Book no: {index + <span class="hljs-number">1</span>}&lt;/h3&gt;
            &lt;p&gt;Book title: {data.title}&lt;/p&gt;
            &lt;p&gt;Book Author: {data.author}&lt;/p&gt;
            &lt;p&gt;
              Completed Status:{<span class="hljs-string">" "</span>}
              {data.isCompleted ? <span class="hljs-string">"Completed"</span> : <span class="hljs-string">"Not Completed"</span>}
            &lt;/p&gt;
            &lt;button onClick={<span class="hljs-function">() =&gt;</span> handleClick(data._id)}&gt;Update&lt;/button&gt;
            {id === data._id &amp;&amp; update &amp;&amp; (
              &lt;&gt;
                &lt;form onSubmit={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> handleUpdate(e, data._id)}&gt;
                  &lt;select name=<span class="hljs-string">"completed"</span>&gt;
                    &lt;option value=<span class="hljs-string">"true"</span>&gt;Completed&lt;/option&gt;
                    &lt;option value=<span class="hljs-string">"false"</span>&gt;Not Completed&lt;/option&gt;
                  &lt;/select&gt;
                  &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span> /&gt;
                &lt;/form&gt;
              &lt;/&gt;
            )}
            &lt;button onClick={<span class="hljs-function">() =&gt;</span> handleDelete(data._id)}&gt;<span class="hljs-keyword">delete</span>&lt;/button&gt;
          &lt;/div&gt;
        );
      })}
    &lt;/div&gt;
  );
};
</code></pre>
<p>This is the basic structure. We displayed the title, author, and status, along with an update and delete button.</p>
<p>Now, let’s add the functionalities.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { book } <span class="hljs-keyword">from</span> <span class="hljs-string">"../types/book.type"</span>;
<span class="hljs-keyword">import</span> { useMutation } <span class="hljs-keyword">from</span> <span class="hljs-string">"convex/react"</span>;
<span class="hljs-keyword">import</span> { api } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../convex/_generated/api"</span>;
<span class="hljs-keyword">import</span> { Id } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../convex/_generated/dataModel"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"../styles/book.css"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Books = <span class="hljs-function">(<span class="hljs-params">{ books }: { books: book[] }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> [update, setUpdate] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [id, setId] = useState(<span class="hljs-string">""</span>);

  <span class="hljs-keyword">const</span> deleteBooks = useMutation(api.queries.deleteBooks);
  <span class="hljs-keyword">const</span> updateStatus = useMutation(api.queries.updateStatus);

  <span class="hljs-keyword">const</span> handleClick = <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    setId(id);
    setUpdate(!update);
  };

  <span class="hljs-keyword">const</span> handleDelete = <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    deleteBooks({ id: id <span class="hljs-keyword">as</span> Id&lt;<span class="hljs-string">"books"</span>&gt; })
      .then(<span class="hljs-function">(<span class="hljs-params">mess</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(mess))
      .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(err));
  };

  <span class="hljs-keyword">const</span> handleUpdate = <span class="hljs-function">(<span class="hljs-params">e: React.FormEvent&lt;HTMLFormElement&gt;, id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    e.preventDefault();
    <span class="hljs-keyword">const</span> formdata = <span class="hljs-keyword">new</span> FormData(e.currentTarget);
    <span class="hljs-keyword">const</span> isCompleted: <span class="hljs-built_in">boolean</span> =
      (formdata.get(<span class="hljs-string">"completed"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>) === <span class="hljs-string">"true"</span>;
    updateStatus({ id: id <span class="hljs-keyword">as</span> Id&lt;<span class="hljs-string">"books"</span>&gt;, isCompleted })
      .then(<span class="hljs-function">(<span class="hljs-params">mess</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(mess))
      .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(err));
    setUpdate(<span class="hljs-literal">false</span>);
  };

  <span class="hljs-keyword">return</span> (
    &lt;div&gt;
      {books.map(<span class="hljs-function">(<span class="hljs-params">data: book, index: <span class="hljs-built_in">number</span></span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> (
          &lt;div
            key={data._id}
            className={<span class="hljs-string">`book-container <span class="hljs-subst">${data.isCompleted ? <span class="hljs-string">"completed"</span> : <span class="hljs-string">"not-completed"</span>}</span>`</span>}
          &gt;
            &lt;h3&gt;Book no: {index + <span class="hljs-number">1</span>}&lt;/h3&gt;
            &lt;p&gt;Book title: {data.title}&lt;/p&gt;
            &lt;p&gt;Book Author: {data.author}&lt;/p&gt;
            &lt;p&gt;
              Completed Status:{<span class="hljs-string">" "</span>}
              {data.isCompleted ? <span class="hljs-string">"Completed"</span> : <span class="hljs-string">"Not Completed"</span>}
            &lt;/p&gt;
            &lt;button onClick={<span class="hljs-function">() =&gt;</span> handleClick(data._id)}&gt;Update&lt;/button&gt;
            {id === data._id &amp;&amp; update &amp;&amp; (
              &lt;&gt;
                &lt;form onSubmit={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> handleUpdate(e, data._id)}&gt;
                  &lt;select name=<span class="hljs-string">"completed"</span>&gt;
                    &lt;option value=<span class="hljs-string">"true"</span>&gt;Completed&lt;/option&gt;
                    &lt;option value=<span class="hljs-string">"false"</span>&gt;Not Completed&lt;/option&gt;
                  &lt;/select&gt;
                  &lt;input <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span> /&gt;
                &lt;/form&gt;
              &lt;/&gt;
            )}
            &lt;button onClick={<span class="hljs-function">() =&gt;</span> handleDelete(data._id)}&gt;<span class="hljs-keyword">delete</span>&lt;/button&gt;
          &lt;/div&gt;
        );
      })}
    &lt;/div&gt;
  );
};
</code></pre>
<p>This is the entire component code. Let me explain what we did.</p>
<p>First, we need to toggle the update, so we defined the <code>handleClick</code> function, and passed a document ID to it.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//handleClick</span>
 <span class="hljs-keyword">const</span> handleClick = <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    setId(id);
    setUpdate(!update);
  };
</code></pre>
<p>In the <code>handleClick</code> you can update the ID state and toggle the update state so that it will toggle the update input when clicked, and on another click, it will close.</p>
<p>Next, we have <code>handleUpdate</code>. We need the document ID to update the data, so we passed the event object as well as the document ID. To get the input, we can use <code>FormData</code>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> updateStatus = useMutation(api.queries.updateStatus);

<span class="hljs-keyword">const</span> handleUpdate = <span class="hljs-function">(<span class="hljs-params">e: React.FormEvent&lt;HTMLFormElement&gt;, id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    e.preventDefault();
    <span class="hljs-keyword">const</span> formdata = <span class="hljs-keyword">new</span> FormData(e.currentTarget);
    <span class="hljs-keyword">const</span> isCompleted: <span class="hljs-built_in">boolean</span> =
      (formdata.get(<span class="hljs-string">"completed"</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>) === <span class="hljs-string">"true"</span>;
    updateStatus({ id: id <span class="hljs-keyword">as</span> Id&lt;<span class="hljs-string">"books"</span>&gt;, isCompleted })
      .then(<span class="hljs-function">(<span class="hljs-params">mess</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(mess))
      .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(err));
    setUpdate(<span class="hljs-literal">false</span>);
  };
</code></pre>
<p>We need to use the <code>useMutation</code> to get the <code>updateStatus</code> function. Pass the ID and the completed status to the function, and handle the asynchronous part using <code>.then</code></p>
<p>For the delete function, the document ID is enough. Just like the previous one, call the delete function using the <code>useMutation</code> and pass the ID to it.</p>
<p>Then pass the document ID and handle the promise.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> deleteBooks = useMutation(api.queries.deleteBooks);

<span class="hljs-keyword">const</span> handleDelete = <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    deleteBooks({ id: id <span class="hljs-keyword">as</span> Id&lt;<span class="hljs-string">"books"</span>&gt; })
      .then(<span class="hljs-function">(<span class="hljs-params">mess</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(mess))
      .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">console</span>.log(err));
 };
</code></pre>
<h2 id="heading-styling">Styling</h2>
<p>Finally, what’s left is to add some styling. I added some basic styling. If the book has not been completed, it will be in red, and if the book has been completed, it will be in green.</p>
<p>Here’s the screenshot:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729428111374/1d1a69ef-5d35-4410-91f4-d8cf4817991d.png" alt="final output" class="image--center mx-auto" width="851" height="601" loading="lazy"></p>
<p>This is it guys!!</p>
<p>You can check my repository for the full code: <a target="_blank" href="https://github.com/sanjayr-12/convex-crud">convex-curd</a></p>
<h2 id="heading-summary">Summary</h2>
<p>In this article, we implemented the CRUD (Create, Read, Update, and Delete) operations by building a book collections app. We begin by setting up Convex and React, and writing CRUD logic.</p>
<p>This tutorial covered both the frontend and the backend, demonstrating how to build a serverless application.</p>
<p>You can find the full code here: <a target="_blank" href="https://github.com/sanjayr-12/convex-crud">convex-curd</a></p>
<p>If there are any mistakes or any doubt contact me on <a target="_blank" href="https://www.linkedin.com/in/sanjay-r-ab6064294/">LinkedIn</a>, <a target="_blank" href="https://www.instagram.com/_sanjayxr_12_/">Instagram</a>.</p>
<p>Thank you for reading!</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
