How I made my own bitly clone using NextJS and FaunaDB 🔥
Tags: typescript, fauna, react
Published: 2020-12-09 15:01:26

I use [bit.ly](https://bitly.com/) 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](https://dev-to-uploads.s3.amazonaws.com/i/vv3dp5a7suy0ks2p34qs.png)

# Tech I Used
- Typescript
- FaunaDB
- NextJS

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

# Code

{% github Manitej66/react-url-shortener no-readme %}

## Creating the NextJS project

Run the below command to start an empty NextJS project

```npm
npx create-next-app url-shortener
```
## Adding TypeScript

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

```npm
yarn add --dev typescript @types/react @types/node
```
rename `_app.js` to `_app.tsx` and paste below code

```ts
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](http://fauna.com)
- 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

```js
{
   "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`

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


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

```ts
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](https://dev-to-uploads.s3.amazonaws.com/i/vv3dp5a7suy0ks2p34qs.png)


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

```ts
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 ⭐**