Build Link Shortener with Supabase and Nuxt
Tags: supabase, nuxt
Published: 2024-04-23 14:17:34

---
title: Build Link Shortener with Supabase and Nuxt
published: true
description: Build Link Shortener with Supabase and Nuxt alternative to bit.ly or dub.co
tags: supabase, nuxt
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
#published_at: 2024-04-23 13:15 +0000
---

Supabase recently [announced](https://supabase.com/blog/postgrest-aggregate-functions) the addition of aggregate functions to their PostgREST API, a feature I've been eagerly awaiting since creating my previous hackathon project, [Supalytic](https://github.com/suciptoid/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](https://supabase.nuxtjs.org), 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](https://github.com/suciptoid/supalink)
- Demo: [spll.lat](https://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:

```bash
# 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
```

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

```js
export default {
  // ...
  modules: [
    '@nuxtjs/supabase'
  ],
  supabase: {
    url: 'YOUR_SUPABASE_URL',
    key: 'YOUR_ANON_KEY'
  }
}
```

Or you can just put on environment variable as:

```
SUPABASE_KEY=yourAnonkey
SUPABASE_URL=yoursupabaseinstanceurl
```

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:

```bash
npx supabase migration new <migration_name>
```
Database migration in this porject will create table: `links`, `analytics`, `orgs`, `org_users` and create `track_link` database function. ([source](https://github.com/suciptoid/supalink/tree/main/supabase/migrations))

## Nuxt UI

To make our development process easier and ensure a polished user interface, we'll use [Nuxt UI](https://ui.nuxt.com), 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`.

```ts
supabase: {
  redirectOptions: {
    login: "/auth/login",
    callback: "/auth/redirect",
    include: ["/org(/*)?"],
  },
},
```

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](https://supabase.nuxtjs.org/get-started#redirectoptions). 

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:

```ts
//...
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;
}
// ...
```

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

```ts
if (link.error.code === "23505")
```

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

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

## 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.

```vue
<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>

```
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:

```typescript
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();
```

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](https://nuxt.com/deploy). 

## 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)](https://supabase.com/ga-week) on this week and also release some anouncement about new features including: [Anonymous Login](https://supabase.com/blog/anonymous-sign-ins) and [S3 Compatible Storage API](https://supabase.com/blog/s3-compatible-storage).