Sucipto
Posted on April 23, 2024
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
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'
}
}
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:
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)
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(/*)?"],
},
},
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;
}
// ...
We need handle duplicate entry error to make sure slug
is not used by another user with this code:
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.
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.
<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:
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.
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.
Posted on April 23, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.