How I made my own bitly clone using NextJS and FaunaDB ๐Ÿ”ฅ

How I made my own bitly clone using NextJS and FaunaDB ๐Ÿ”ฅ

ยท

7 min read

I use bit.ly a lot to shorten my URLs but their dashboard is cluttered. I mean there's a lot more on the website which I didn't like. So I tried to make something similar but with only focus on shortening links. So, here is what I did.

Alt Text

Tech I Used

  • Typescript
  • FaunaDB
  • NextJS

This is the first time I'm working with FaunaDB and TypeScript so I'm super excited!

Code

Creating the NextJS project

Run the below command to start an empty NextJS project

npx create-next-app url-shortener

Adding TypeScript

Create a tsconfig.json file in the root folder and run the below command.

yarn add --dev typescript @types/react @types/node

rename _app.js to _app.tsx and paste below code

import type { AppProps /*, AppContext */ } from "next/app";
import "../styles/globals.css";

function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

export default MyApp;

Dependencies

  • axios (for API Calls)
  • faunadb (for serverless DB)
  • generate-unique-id (for generating short URLs)

Creating Database

  • Go to faunadb
  • Create a free account and login
  • Create a database
  • Create a collection named urls
  • Go to keys section and create a key and copy it
  • Create a .env.local file in root folder and paste the key as
NEXT_PUBLIC_FAUNA_KEY=YOUR_KEY

Main Logic

The idea is to store a JSON Object of the below format

{
   "url":"https://dev.to",
   "short_url":"547382"
}

Whenever a user enters {your-domain}/547382 they will be redirected to https://dev.to

Writing serverless functions

To make a short URL from original URL

Go to pages/api and create a file createUrl.ts

import type { NextApiRequest, NextApiResponse } from "next";
const generateUniqueId = require("generate-unique-id");
const faunadb = require("faunadb"),
  q = faunadb.query;

const client = new faunadb.Client({
  secret: process.env.NEXT_PUBLIC_FAUNA_KEY,
});

export default async (req: NextApiRequest, res: NextApiResponse) => {
  const { url } = req.body;

  const id = generateUniqueId({
    length: 8,
    useLetters: false,
  });

  try {
    const info = await client.query(
      q.Create(q.Collection("urls"), {
        data: {
          ourl: url,
          surl: id,
        },
      })
    );

    res.status(200).send(id);
  } catch (error) {
    res.status(400).send(error.message);
  }
};

To get original URL from short URL

Go to pages/api and create a file getShortUrl.ts

import type { NextApiRequest, NextApiResponse } from "next";
const faunadb = require("faunadb"),
  q = faunadb.query;

const client = new faunadb.Client({
  secret: process.env.NEXT_PUBLIC_FAUNA_KEY,
});

export default async (req: NextApiRequest, res: NextApiResponse) => {
  try {
    const ourl = await client.query(
      q.Map(
        q.Paginate(q.Match(q.Index("get_short_url"), req.body.url)),
        q.Lambda("X", q.Get(q.Var("X")))
      )
    );

    res.send(ourl.data[0].data.ourl);
  } catch (error) {
    res.status(400).send(error.message);
  }
};

That's it for the backend!

Frontend

We basically need 2 routes

  1. To create short URLs
  2. To redirect users

1. To create short URLs

pages/index.tsx

import Axios from "axios";
import React, { useState } from "react";
import Head from "next/head";
const index = () => {
  const [url, setUrl] = useState<string>("");
  const [surl, setsUrl] = useState<string>("");
  const [load, setLoad] = useState<boolean>(false);
  const home =
    process.env.NODE_ENV === "development" ? "localhost:3000" : "zf.vercel.app";

  const getShortUrl = async () => {
    setLoad(true);
    await Axios.post("/api/createUrl", {
      url: url,
    })
      .then((res) => {
        setsUrl(`${home}/${res.data}`);
        setLoad(false);
      })
      .catch((e) => console.log(e));
  };
  return (
    <div className="container">
      <Head>
        <link rel="preconnect" href="https://fonts.gstatic.com" />
        <link
          href="https://fonts.googleapis.com/css2?family=Acme&display=swap"
          rel="stylesheet"
        />
        <title>URL Shortener ๐Ÿฑโ€๐Ÿš€</title>
      </Head>
      <h1 className="title">
        URL Shortener <span>๐Ÿ˜Ž</span>
      </h1>
      <input
        className="inp"
        placeholder="enter URL to be shorten"
        onChange={(e) => setUrl(e.target.value)}
      />
      <style jsx>{`
        .container {
          display: flex;
          padding: 10px;
          flex-direction: column;
          justify-content: center;
          align-items: center;
        }
        .title {
          font-family: "Acme", sans-serif;
          font-size: 20px;
        }
        .inp {
          padding: 20px;
          margin: 10px;
          width: 80%;
          border-radius: 5px;
          border: 1px solid #000;
          border-radius: 5px;
          text-align: center;
          font-family: "Acme", sans-serif;
          font-size: 20px;
        }
        .btn {
          padding: 10px 20px;
          margin: 10px;
          border: none;
          background: #3254a8;
          color: white;
          border-radius: 10px;
          font-family: "Acme", sans-serif;
          font-size: 20px;
          cursor: pointer;
        }
        .surl {
          font-family: "Acme", sans-serif;
          padding: 10px;
          margin: 10px;
          background-color: #32a852;
          border-radius: 10px 20px;
          color: white;
        }
      `}</style>
      <button onClick={getShortUrl} className="btn">
        {load ? "loading" : "Shorten"}
      </button>
      {surl.length > 0 ? <p className="surl">{surl}</p> : null}
    </div>
  );
};

export default index;

Output

Alt Text

2. To create a redirect route

This part is tricky, we don't need to display anything to the user in this route. We simply need to redirect to the original URL from the query in the URL

pages/[url].tsx

import Axios from "axios";
import { GetServerSideProps } from "next";

const Url = () => {
  return null;
};

export const getServerSideProps: GetServerSideProps = async (context: any) => {
  const { url } = context.params;

  const home =
    process.env.NODE_ENV === "development"
      ? "http://localhost:3000"
      : "https://zf.vercel.app";

  const info = await Axios.post(`${home}/api/getShortUrl`, {
    url: url,
  });
  return {
    redirect: {
      destination: info.data,
      permanent: true,
    },
  };
};

export default Url;

That's it! To run locally use the below command

yarn dev

Give a โญ