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?
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.
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 .
After that, we will have a default application like this:
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:
Then crate an airtable base like this
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
2, Make a simple UI to short and manage URL:
The Save button and URL list will be displayed only when user logged-in
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
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