Build Link Shortener with Supabase and Nuxt

suciptoid

Sucipto

Posted on April 23, 2024

Build Link Shortener with Supabase and Nuxt

Supabase recently announced the addition of aggregate functions to their PostgREST API, a feature I've been eagerly awaiting since creating my previous hackathon project, Supalytic. with this feature, we can reduce the amount of Postgres functions (called via RPC) and use aggregates directly from the client SDK.

In this blog post, we will create a link shortener app (similar to bit.ly or dub.co) using NuxtJS and Supabase as the backend, leveraging Supabase's auth and database features. The Nuxt ecosystem already has an official Supabase module, making it easier to integrate Supabase with NuxtJS.

TLDR;

  • Create Supabase & NuxtJS project
  • Install Supabase (@nuxtjs/supabase) & Nuxt UI (@nuxt/ui) module
  • Use new supabase feature: Aggregate function sum()
  • Source Code: suciptoid/supalink
  • Demo: spll.lat

Initialize Project

To initialize the project, we need to create a Supabase project and grab the anonKey and supabaseUrl.

After creating the project, you'll find the anonKey and supabaseUrl in the project's settings. We'll need these values to connect our NuxtJS application to the Supabase backend.

Next, we'll set up the NuxtJS project:

# Create a new NuxtJS project
npx nuxi init supalink

# Change to the project directory
cd supalink

# Install the Supabase NuxtJS module
npm install @nuxtjs/supabase
Enter fullscreen mode Exit fullscreen mode

In the nuxt.config.js file, add the Supabase module and configure it with the anonKey and supabaseUrl from your Supabase project:

export default {
  // ...
  modules: [
    '@nuxtjs/supabase'
  ],
  supabase: {
    url: 'YOUR_SUPABASE_URL',
    key: 'YOUR_ANON_KEY'
  }
}
Enter fullscreen mode Exit fullscreen mode

Or you can just put on environment variable as:

SUPABASE_KEY=yourAnonkey
SUPABASE_URL=yoursupabaseinstanceurl
Enter fullscreen mode Exit fullscreen mode

With the project initialized and the Supabase module configured, we're ready to start building our link shortener application.

Database Migration

In this project we will create a few database migration using supabase cli command:

npx supabase migration new <migration_name>
Enter fullscreen mode Exit fullscreen mode

Database migration in this porject will create table: links, analytics, orgs, org_users and create track_link database function. (source)

Nuxt UI

To make our development process easier and ensure a polished user interface, we'll use Nuxt UI, a comprehensive UI library designed specifically for NuxtJS applications. By incorporating Nuxt UI, we can rapidly build our link shortener app with pre-built, high-quality components, aligning with Supabase's motto: "Build in a weekend, scale to millions."

Authentication

For authentication, we will use Supabase Auth and need to enable some auth providers on the Supabase project dashboard. For this project, I will use GitHub and Google providers.

Nuxt Supabase already provides an auth middleware for protected routes, which we can configure in nuxt.config.ts.

supabase: {
  redirectOptions: {
    login: "/auth/login",
    callback: "/auth/redirect",
    include: ["/org(/*)?"],
  },
},
Enter fullscreen mode Exit fullscreen mode

The redirectOptions configuration defines the behavior for authentication redirects:

  • login: The path to redirect to for login.
  • callback: The path to handle the redirect after successful authentication.
  • include: An array of paths that require authentication.

Detailed documentation can be found here.

With this configuration, Nuxt Supabase will handle the authentication flow and provide convenient methods to interact with the Supabase Auth API.

Create Link

This step involves creating a dedicated Nuxt page and a Vue component to manage link creation. We'll utilize Nuxt UI components like Modal, Form, useCopyToClipboard(), and Notification (Toast) to provide a user-friendly experience.

The form submission handler ensures data validation and informative feedback:

//...
const link = await supabase
  .from("links")
  .insert({
    org_id: orgId,
    url: state.link,
    slug: state.slug,
  })
  .select("id,slug")
  .single();

if (link.error) {
  if (link.error.code === "23505") {
    toast.add({
      id: "create_link_error",
      title: "Error creating link",
      description: "Link already in use",
    });
  } else {
    toast.add({
      id: "create_link_error",
      title: "Error creating link",
      description: link.error.message,
    });
  }
  return;
}
// ...
Enter fullscreen mode Exit fullscreen mode

We need handle duplicate entry error to make sure slug is not used by another user with this code:

if (link.error.code === "23505")
Enter fullscreen mode Exit fullscreen mode

Finally, the code copies the generated short URL to the clipboard using useCopyToClipboard(), displays a success toast notification, and closes the modal component.

const { copy } = useCopyToClipboard();
//...
copy(`${url.origin}/${link.data!.slug}`, {
  title: "Link Created",
  description: "Link created and copied to clipboard",
});
Enter fullscreen mode Exit fullscreen mode

Link Redirect

When a user clicks a shortened URL, we need to send them to the original target URL. To handle this, we create a special Nuxt page at /pages/[link].vue.

This page component uses the [link] part of the URL to capture the shortened slug. It then queries the links table in Supabase using the slug (which is indexed for faster lookups).

If the slug is found, the code grabs the target URL and redirects the user there. Otherwise, the user is redirected to the homepage / our landing page.

<script setup lang="ts">
import type { Database } from "~/supabase/types";

const route = useRoute();

const supabase = useSupabaseClient<Database>();
const link = await supabase
  .from("links")
  .select("id,org_id,slug,url")
  .eq("slug", route.params.link)
  .single();

if (link.data) {
  // Navigating to link
  const track = await supabase.rpc("track_link", { link_id: link.data.id });
  await navigateTo(link.data.url, { external: true });
} else {
  await navigateTo("/");
}
</script>

<template>
  <div>Redirecting...</div>
</template>

Enter fullscreen mode Exit fullscreen mode

Additionally, it calls a Supabase function using supabase.rpc("track_link", { link_id: link.data.id }) to track the link click and update an hourly click counter in the database.

Link Analytics

As mentioned earlier, Supabase now supports aggregate functions directly within the Supabase SDK (via PostgREST). We can leverage this feature to efficiently calculate total link clicks for our project.

The provided code snippet showcases how to achieve this:

let query = supabase
  .from("analytics")
  .select("total:clicks.sum(), links!inner(org_id)")
  .eq("links.org_id", route.params.org_id);

if (route.query.link) {
  query = query.eq("links.id", route.query.link);
}

return await query.single();
Enter fullscreen mode Exit fullscreen mode

The provided code snippet queries the analytics table using the sum() function to calculate the total number of clicks on current Org. Additionally, if the URL contains a link query parameter, it further filters the results to include statistics for that specific link using route.query.link.

Deployment

For deployment we can use Cloudflare Pages or another deployment platform, you can read more deployment guide on Nuxt Docs.

Conclusion

This exploration showcased the smooth integration of Supabase with NuxtJS 3. Supabase + Supabase Nuxt modules proved invaluable, saving development time.

Shoutout to Supabase! Huge congratulations to @supabase_io for being Generally Available (GA) on this week and also release some anouncement about new features including: Anonymous Login and S3 Compatible Storage API.

💖 💪 🙅 🚩
suciptoid
Sucipto

Posted on April 23, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related