Un acortador de links con Next.js y Supabase
El diseño básico de la base de datos de un acortador de links, y su implementación en una aplicación real con Next.js y Supabase.
Los acortadores de enlaces sirven para simplificar la manera en que compartimos y gestionamos los enlaces en línea. En el siguiente artículo verás la estructura básica de la base de datos para desarrollar un proyecto de este tipo, y el proyecto implementado en Next.js con ayuda de Supabase para la autenticación y la base de datos. Cabe aclarar que Supabase simplifica el desarrollo de aplicaciones al proporcionar una infraestructura de backend completa y fácil de usar.
Estructura de la base de datos
Para este proyecto se utilizará una estructura básica de base de datos, para simplificar el desarrollo de la aplicación. Tendremos solo tres tablas: users, links y visits_links.
¿Qué se guardará en cada tabla?
- Users: todos los usuarios autenticados. Se almacenará su nombre, avatar y fecha de creación.
- Links: los links creados por los usuarios de la aplicación. Se guardará el nombre, la URL personalizada o corta, la URL original, el id del usuario que la creó y la fecha de creación.
- Visits_links: todas las visitas o clics que hagan las personas a tu enlace personalizado. Se almacenará la fecha de creación y el id del link al que se hizo clic.

Cabe resaltar que links tendrá como llave foránea el id de users, y visits_links tendrá como llave foránea el id de la tabla links.
Puntos clave de la aplicación
De manera general verás los puntos clave de la aplicación para que tenga el funcionamiento que queremos. Tú puedes organizar las rutas de la aplicación como quieras. Todo estará resumido en este artículo; para ver el código completo de la aplicación, consulta el repositorio.
Nos saltaremos toda la conexión con Supabase y cómo hacer la autenticación con GitHub. Estos pasos los puedes conseguir en la documentación de Supabase.
Crear el cliente de Supabase en el servidor
export function createClient() {
const cookieStore = cookies()
// El tipo Database se puede obtener gracias a un comando de Supabase.
// Para más información, consulta la documentación de Supabase
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
},
},
)
}Crear el cliente de Supabase en el cliente
'use client'
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
)
}Cómo guardar los links
Para guardar los links utilizamos los server actions de Next.js.
export const FormCreateLink = () => {
const addLink = async (formData: FormData) => {
'use server'
// Obtenemos el name y la url del formulario
const name = formData.get('name')?.toString().replace(' ', '-')
const url = formData.get('url')?.toString()
// Verificamos si la url comienza con el protocolo http
if (!url.startsWith('http')) {
throw new Error('URL inválida')
}
// Verificamos que el name no exista
const supabase = createClient()
const { data, error: errorSelect } = await supabase.from('links').select('*').eq('name', name)
if (errorSelect) {
throw new Error('Error al guardar el enlace')
}
if (data.length !== 0) {
throw new Error('El nombre ya existe')
}
// Guardamos el link
const {
data: { user },
} = await supabase.auth.getUser()
const shortUrl = `https://dominio.ejemplo/${name}`
const { error } = await supabase
.from('links')
.insert({ name, url, short_url: shortUrl, uid_user: user?.id })
if (error) {
throw new Error('Error al guardar el enlace')
}
}
return (
<form action={addLink}>
<input type="text" name="name" placeholder="Nombre del enlace" required />
<input type="text" name="url" placeholder="URL" required />
<button>Guardar enlace</button>
</form>
)
}Cómo editar los links
Recuerda que para ver, guardar, actualizar y borrar debes editar las políticas de la base de datos de Supabase.
interface Props {
link: TypeLink
}
export const FormEditLink: FC<Props> = ({ link }) => {
const editLink = async (formData: FormData) => {
'use server'
// Obtenemos el name del formulario
const name = formData.get('name')?.toString().replace(' ', '-')
// Verificamos que el name no exista
const supabase = createClient()
const { data, error: errorSelect } = await supabase.from('links').select('*').eq('name', name)
if (errorSelect) {
throw new Error('Error al guardar el enlace')
}
if (data.length !== 0) {
throw new Error('El nombre ya existe')
}
// Guardamos el link editado
const shortUrl = `https://dominio.ejemplo/${name}`
const { error } = await supabase
.from('links')
.update({ name, short_url: shortUrl })
.eq('id', id)
if (error) {
throw new Error('Error al guardar el enlace')
}
}
return (
<form action={editLink}>
<input type="text" name="name" placeholder="Nombre del enlace" />
<button>Editar enlace</button>
</form>
)
}Cómo borrar los links
Para borrar un link podríamos utilizar un botón con la siguiente funcionalidad.
export const DeleteButton = () => {
const handleDelete = async () => {
const { error } = await supabase.from('links').delete().eq('id', link.id)
if (error) {
throw new Error('Error al eliminar el link')
}
}
return (
<button type="button" onClick={handleDelete}>
{/* Ícono de la caneca de basura */}
<TrashIcon />
</button>
)
}Redireccionamiento a la URL original
Este es el punto clave de la aplicación. Vamos a crear la ruta /[name], que nos va a servir para obtener el nombre del link, buscar el enlace por ese nombre en la base de datos y redirigir a la URL original.
Por ejemplo, un usuario x da clic en el enlace https://dominio.ejemplo/rubertweb; la aplicación extrae el nombre de la URL, rubertweb, obtiene de la base de datos el link que coincide con ese nombre y redirige a la URL original, que en este caso sería https://rubertweb.dev.
Recordemos que el link que se obtiene de la tabla links tiene las propiedades nombre, URL personalizada o corta, URL original, id del usuario que la creó y fecha de creación.
El código del archivo /[name]/page.tsx quedaría de la siguiente manera:
interface Props {
params: {
name: string
}
}
const RedirectingPage: FC<Props> = async ({ params: { name } }) => {
const supabase = createClient()
const { data: links, error } = await supabase
.from('links')
.select('*')
.eq('name', decodeURIComponent(name))
if (error != null || links == null || links?.length === 0) {
return redirect('/')
}
return redirect(links[0].url)
}
export default RedirectingPageCómo guardar los clics de los enlaces personalizados
Para guardar el clic hecho en un enlace vamos a agregar una funcionalidad al componente anterior.
interface Props {
params: {
name: string
}
}
const RedirectingPage: FC<Props> = async ({ params: { name } }) => {
const supabase = createClient()
const { data: links, error } = await supabase
.from('links')
.select('*')
.eq('name', decodeURIComponent(name))
if (error != null || links == null || links?.length === 0) {
return redirect('/')
}
// La siguiente línea inserta en la tabla visits_links un nuevo registro con el id del link.
// La base de datos agrega el id y la fecha de creación automáticamente
await supabase.from('visits_links').insert({ uid_link: links[0].id })
return redirect(links[0].url)
}
export default RedirectingPageCon estos componentes tendrás las funcionalidades básicas de la aplicación. Recuerda que puedes ver el código completo del ejemplo en mi repositorio de GitHub. También me puedes escribir desde la sección de contacto.

