URL shortener and manager with Clerk x NextJS x TailwindCSS

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: 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: bit.ly/36E1sRy