URL shortener and manager with Clerk x NextJS x TailwindCSS

Hi Hashnoder,

I have built a URL shortener for the Hackathon on Hashnode in partnership with Clerk.

  • Problems: We have a lot of tool to short an URL, but most of them are not easy to use, some ads and some no needed function. We want a simple shortener, that allow us to short URL in just a click, no ads, no price.

  • So, I have built a very simple, clean and fast URL shortener. It 's beshort .

    beshort is mainly built on NextJS , Clerk , TailwindCSS , AirTable as DB and be hosted by vercel.

I will tell more about Clerk.

What is Clerk?

image.png

With Clerk, you can add beautiful, high-conversion Sign Up and Sign In forms to your React application in minutes. After signing in, Clerk empowers your users to take control of their account security with multi-factor authentication and device management. If you've ever found yourself thinking, "there's got to be a better way to build auth" - Clerk was built with you in mind.

It truly only takes minutes to add best-in-class authentication experiences to your application - and Clerk's team is constantly working behind-the-scenes to make them even better.

image.png

Step to setup:

1, Sign up for a free Clerk "Starter" plan by visiting this link .

2, Go to Dashboard and create new application with NextJS and Vercel then do some project settings. More details, watch this video .

create.PNG

After that, we will have a default application like this:

after.PNG

3, Config somethings:

  • We need bittly API key to make URL shorten
  • We need AirTable key to manipulate data Then set keys to vercel env variable like this:

key.PNG

Then crate an airtable base like this

image.png

Now start coding

1, We need some dependencies:

  • airtable: is SDK to do CRUD with AirTable.
  • axios: to make request to bittly for shorten URL
  • taildwindcss and postcss: to do CSS by class name
  • react-toastify: to do notification
  • unstated-next: to manage state without redux packages.PNG

2, Make a simple UI to short and manage URL:

The Save button and URL list will be displayed only when user logged-in

ui.PNG

3, Write some functions:

  • URL validate
function validURL(str) {
    var pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol
      '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
      '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
      '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
      '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
      '(\\#[-a-z\\d_]*)?$','i'); // fragment locator
    return !!pattern.test(str);
}
  • Call bittly API to get shorten URL
const getShortUrl = async function(longUrl) {
    if (!longUrl || !validURL(longUrl)) {
        return { error: 'Please input a valid URL' }
    }
    const header = {
        headers: { 
            Authorization: "Bearer " + process.env.NEXT_PUBLIC_BEARER_TOKEN,
            'Content-Type': 'application/json'
        }
    }
    const body = {
        "long_url": longUrl
    }
    let result = {}
    await axios.post(process.env.NEXT_PUBLIC_SHORT_END_POINT, body, header)
    .then(res => {
        const { data: { link }} = res
        result = { link }
    })
    .catch(error => {
        result = { error: 'Server error!'}
        let { response: { data: { message }}} = error
        if (message){
            message === 'ALREADY_A_BITLY_LINK' && (message = 'This is already a bittly link.')
            result = { error: message }
        }
    })
    return result
}
  • copy button
onClick={() => {
              navigator.clipboard.writeText(shortedUrl)
              toastInfo('copied!')
            }
  • setup airtable service
var Airtable = require("airtable")
var base = new Airtable({
  apiKey: process.env.NEXT_PUBLIC_AIR_TABLE_TOKEN,
}).base(process.env.NEXT_PUBLIC_AIR_TABLE_BASE)
const tableName = "user_url"
const air = base(tableName)
  • to create new airtable record
const createURL = async function (data) {
  let result = {
    success: false,
  }
  await air.create([{ ...data }]).then(res => result = {
          success: true,
          newUrl: res[0].fields,
        })
  return result
}
  • to get URL list by userId
/**
 * Get user URL from Air Table
 * @param {*} userId
 * @returns array of urls
 */
const getUserUrl = async function (userId) {
  if (!userId) {
    return []
  }
  try {
    const res = await air
      .select({
        filterByFormula: "{user_id} = '" + userId + "'",
        sort: [{field: "created_at", direction: "desc"}]
      })
      .all()
    return parseAirTableResponse(res)
  } catch (error) {
    return {
      err: error,
    }
  }
}
/**
 * Parse Air Table response to URL array
 * @param {*} res
 * @returns
 */
function parseAirTableResponse(res) {
  if (!res || !res.length) {
    return []
  }
  return res.map((row) => {
    const fields = row.fields
    return {...fields, airId: row.id}
  })
}
  • to delete a record
const removeURL = async function (id) {
  let result = {
    success: false,
  }
  await air.destroy([id]).then(res => result = { success: true}) 
  return result
}
  • seting toastify to show message
import { toast } from 'react-toastify';
const toastOption = {
  position: toast.POSITION.TOP_CENTER,
  autoClose: 1500,
  closeButton: true,
  hideProgressBar: true,
}

export const toastInfo = (message) => {
  toast.success(message, toastOption)
}
export const toastError = (message) => {
  toast.warn(message, {...toastOption, autoClose: 2000})
}

Deployment

  • Push code to master branch of repo that you select when create Vercel app
  • Vercel will do the rest for you, easy.

Here is github: https://github.com/hieudien/beshort You can add any function by create new Pull Request, but remember to keep it clean and simple.

Thanks for reading.

Linkedin post: https://bit.ly/36E1sRy

29
Subscribe to my newsletter

Read articles from Đoàn Trọng Hiếu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Đoàn Trọng Hiếu
Đoàn Trọng Hiếu