Cómo hacer un acortador de URLs con Next.js y Prisma

16 julio, 2021

9 minutos de lectura

💻 DesarrolloNext.jsPrisma

¿Ves alguna errata o quieres modificar algo? Haz una Pull Request

En este artículo aprenderás a crear un acortador de urls, desde cero y paso a paso.

Un acortador es un servicio que posiblemente ya hayas utilizado, como por ejemplo bit.ly. Sirve para, dada una URL, nos genera una URL más corta que podemos utilizar para ahorrarnos caracteres o si no tenemos acceso a las analíticas de la web en cuestión, saber cuantos clicks se han hecho desde determinado enlace.

Para ello vamos a utilizar las siguientes tecnologias:

  • React / Next.js como Framework.
  • PostgreSQL como base de datos.
  • Prisma como ORM para modear la base de datos y realizar las consultas.

Let's go!

Generar el proyecto

Lo primero será crear una aplicación Next.js, para ello utilizaremos el CLI de Next.js:

$ npx create-next-app url-shortener

Esto nos instala las dependencias necesarias y nos crea una estructura de proyecto y carpetas de nuestra aplicación.

Lo primero que vamos a hacer es crear el formulario de envio de URLs en la parte frontend de nuestra aplicación.

Nos dirigimos al fichero pages/index.js, borramos lo que viene por defecto y lo dejamos así:

import Head from 'next/head'
import styles from '../styles/Home.module.css'

export default function Home () {
  return (
    <div className={styles.container}>
      <Head>
        <title>URL Shortener</title>
        <meta name='description' content='Generated by create next app' />
        <link rel='icon' href='/favicon.ico' />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>URL Shortener</h1>

        <p className={styles.description}>Acorta tus URLs aquí</p>

        <div className={styles.grid}>
          <form className={styles.card}>
            <input type='text' className={styles.input} placeholder='URL' />
            <button className={styles.button}>Acorta</button>
          </form>
        </div>
      </main>
    </div>
  )
}

Formulario de envío de URLs

Vamos a hacer uso de algunos hooks de React para ayudarnos con el formulario de envío de URLs.

import { useRef, useState } from 'react'

Con useRef guardamos la referencia del input, y useState lo usaremos para almacenar la URL que se genera al presionar el botón.

const inputRef = useRef()
const [shortURL, setShortURL] = useState('')

Actualizamos el formulario aplicando estas variables:

<input
  ref={inputRef}
  className={styles.input}
  type="text"
  placeholder="URL"
/>
<button className={styles.button}>Acorta</button>
<span className={styles.input}>{shortURL}</span>

Creamos una función para manejar el estado de 'on submit' del firmulario. Basicamente lo que haremos es recoger el valor de la URL del input y crear nuestra URL corta. Lo podríamos hacer en local, pero lo ideal es que guardemos estos valores en una base de datos para que persistan y por tanto se pueda utilizar como un servicio.

Añadimos lo siguiente al formulario:

<form className={styles.card} onSubmit={handleSubmit}>
  ...
</form>

Y creamos la función:

const handleSubmit = e => {
  e.preventDefault()
  const url = inputRef.current.value
  //TODO: Peticion al API
}

Servicio REST para crear la URL corta

Continuamos, y vamos a crear el servicio que nos crea la URL corta, lo almacena en la base de datos y la envía al frontend. Para ello vamos a la carpeta pages/api y creamos un nuevo fichero, llamado por ejemplo shortUrl.js

Ahi creamos una función serverless que consumiremos via AJAX y tiene la siguiente pinta:

export default async function handler (req, res) {
  const { url } = req.body
  const shortUrl = Math.random()
    .toString(36)
    .substr(2, 6)

  res.status(200).send({ url, shortUrl })
}

Tomamos la url del cuerpo de la petición que ahora implementaremos y generamos un string de 5 caracteres aleatorios que nos servirá como la URL corta. Antes de hacer nada con la base de datos, enviamos la respuesta y probamos a ver si todo llega y se genera bien.

Volvemos a pages/index.js y ahora vamos a hacer una petición fetch a la URL api/shortUrl que corresponde con la función que acabamos de crear.

const handleSubmit = e => {
  e.preventDefault()
  const url = inputRef.current.value

  fetch('/api/shortUrl', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ url })
  })
    .then(res => res.json())
    .then(data => {
      console.log(data)
    })
}

Si probamos esto, vemos que la consola muestra que le llega la respuesta con la URL enviada y la ShortURL generada.

Console.log mostrando los datos recibidos

Podemos incluso hacer uso de useState y dejar ya la URL corta lista para usar:

...
 .then(data => { setShortURL(data.shortUrl) });

Y asi al generarse, la función setShortUrl la coloca en el elemento span que teniamos: setState colocando la respuesta recibida

Almacenamiento en base de datos, con Prisma

Todo esto está muy bien, pero si esto lo queremos llevar a producción y que las URLs cortas se queden almacenadas y asi poderlas usar apropiadamente necesitamos persisitir todo esto.

Vamos a utilizar PostgreSQL para almacenar las URLs cortas y Prisma como ORM para facilitarnos la creación de las tablas y las consultas SQL.

Primero necesitamos la base de datos. Yo voy a usar PostgreSQL en local, pero puedes utilizar servicios cloud como Digital Ocean , Amazon RBD, Google Cloud SQL, Supabase, etc... que ofrecen "SQLs as a Service", lo unico que necesitas es una 'URL String' como la siguiente:

postgresql://carlosazaustre:carlosazaustre@localhost:5432/links_test?schema=public"

Donde:

  • carlosazaustre:carlosazaustre es mi user y password en local para la base de datos.
  • localhost es el host de mi servidor local.
  • 5432 es el puerto de mi servidor local.
  • links_test es el nombre de mi base de datos.

Este url string lo necesitaremos dentro de poco, antes, vamos a instalar Prisma:

$ npm install -D prisma
$ npm install @prisma/client

Una vez instalado, corremos el siguente comando para iniciar el CLI de Prisma y que nos genere, el archivo de Schemas:

$ npx prisma init

Esto nos crea entre otras cosas el fichero prisma/schema.prisma donde se indica el motor de base de datos y los modelos que vamos a emplear. editamos ese fichero con lo siguiente:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Link {
  id         Int       @id @default(autoincrement())
  url        String    @unique
  createdAt  DateTime  @default(now())
  shortUrl   String    @unique
}

Hemos creado el modelo Link que mapeará a una Tabla SQL, con un id autoincremental, la url que pasamos por formulario, la shortUrl que generamos y la fecha de creación que lo hace de forma automática.

Ahora abrimos el fichero .env para colocar el URL string de conexión a la base de datos.

Este fichero no debe subirse al repositorio ya que tu usuario y contraseña quedarían expuestos.

DATABASE_URL="postgresql://carlosazaustre:carlosazaustre@localhost:5432/_test?schema=public"

Con todo esto, ejecutamos los siguientes comandos que generarán las instrucciones SQL para crear las tablas en la base de datos.

$ npx prisma generate
$ npx prisma migrate dev --name init
$ npx prisma db push

Si accedemos por terminal a PostreSQL, vemos la base de datos creada y la tabla Links:

$ psql links_test
links_test-# \d
                    List of relations
 Schema |        Name        |   Type   |     Owner
--------+--------------------+----------+----------------
 public | Link               | table    | carlosazaustre
 public | Link_id_seq        | sequence | carlosazaustre
 public | _prisma_migrations | table    | carlosazaustre
(3 rows)

Listo, ya podemos hacer uso de Prisma para almacenar los datos en Postgres. Vayamos a nuestro fichero pages/api/shortUrl.js y modifiquemos la función serverless.

import { PrismaClient } from '@prisma/client'

export default async function handler (req, res) {
  const prisma = new PrismaClient()
  const { url } = req.body
  const shortUrl = Math.random()
    .toString(36)
    .substr(2, 6)

  try {
    const data = await prisma.link.create({
      data: { url, shortUrl }
    })

    prisma.$disconnect()

    return res.status(200).send(data)
  } catch (error) {
    console.log(error)
    return res.status(500).send(error)
  }
}

Importamos el cliente Prisma, y con el metodo prisma.link.create creamos un nuevo registro en la tabla "Links", que consta de una url y una shortUrl. ya que el Id y la fecha se autogeneran al momento de crear.

URL corta enviada al frontend Registro almacenado en la base de datos

Una vez creado, mandamos la respuesta al frontend y como ya tenemos la petición fetch implementada, todo debería funcionar y recibir la URL corta y esta quedarse almacenada en la base de datos

Redirección a la URL original

Ahora nos queda la funcionalidad mas importante y es, que cuando pongamos una URL corta, esta redirija a la URL original. ¿Cómo hacemos esto? Pues con dos propiedades de Next, es muy sencillo.

Primero de todo vamos a crear una ruta dinámica. En Next se hace creando un fichero en la carpeta pages con el nombre entre corchetes. Tal que así:

pages/[shortId].js

Con esto indicamos a Next que la ruta será dinámica, y el parámetro o nombre de la ruta, se recoge en la variable params.shortId

Puedes llamarlo shortId o como quieras.

Implementamos la ruta dinámica en el fichero pages/[shortId].js creando un componente muy simple ya que nunca se renderizará esta parte

export default function ShortIdPage () {
  return <div>ShortID Redirect</div>
}

Lo interesante viene ahora. Usamos la función de Next getServerSideProps para transformar esta "página" en una página renderizada desde el servidor (SSR - Server Side Renderer) Aquí tomaremos el valor dinamico de la ruta, a través del objeto params, y este lo utilizaremos para hacer una query a nuestra base de datos y que nos devuelva el registro para el que coincida ese shortId.

import { PrismaClient } from "@prisma/client";

...

export async function getServerSideProps({ params }) {
  const prisma = new PrismaClient();
  const { shortId } = params;

  const data = await prisma.link.findUnique({
    where: { shortUrl: shortId },
  });

Esta query a la base de datos la podemos colocar ahi, porque eso es código que no viaja al cliente, se hace en el lado del servidor. Aunque veas código React ahi, es la peculiaridad que tiene Next, que es un framework que junta ambos mundos, el Frontend y el Backend y a veces es dificil de distinguir.

Ahora necesitamos hacer que la página redirija. Para ello usaremos la propiedad redirect que nos proporciona Next para que ocurra:

if (!data) {
  return {
    redirect: { destination: '/' }
  }
}

return {
  redirect: {
    destination: data.url
  }
}

Si la data recibida no contiene nada, redirigimos por defecto a la ruta raiz, pero si el objeto data, contiene una URL (la que teniamos almacenada en la DB) lo que hacemos es que redirija a ella.

Este es el fichero pages/[shortId].js completo:

import { PrismaClient } from '@prisma/client'

export default function ShortIdPage () {
  return <div>ShortID Redirect</div>
}

export async function getServerSideProps ({ params }) {
  const prisma = new PrismaClient()
  const { shortId } = params

  const data = await prisma.link.findUnique({
    where: { shortUrl: shortId }
  })

  prisma.$disconnect()

  if (!data) {
    return {
      redirect: { destination: '/' }
    }
  }

  return {
    redirect: {
      destination: data.url
    }
  }
}

Código del proyecto

Aquí tienes el repositorio en GitHub con todo el código visto en el tutorial. Por si quieres revisarlo.

Y con esto ya tendrías tu propio acortador de URLs. Necesitas tener la Base de datos en producción. Puedes elegir como digo al principio cualquier servicio Cloud como Digital Ocean Databases , Amazon RDS, Google Cloud SQL, Supabase, PlanetScale, etc..., y subir el código también a producción, puedes hacerlo en Vercel, Heroku, ... donde prefieras.

Yo mismo tengo un servicio de acortador de URLs en producción, siguiendo este mismo código, pero añadiéndole autenticación con Auth0 y mejor estructurado . El código es Open Source y lo puedes ver en este otro repositorio de GitHub , y el servicio en producción lo tienes en czstr.link

¡Espero que te haya gustado y servido! Compártelo con tus amigos y siéntete libre de hacer pull requests para mejorar este tutorial.

© 2023 Carlos Azaustre | Made with 💻 in 🇪🇸