Read if:
- No reasons needed
Resources
- Source Code : Github
- Live Example : shrink.theether.live
- Inspiration : @prajyu How to make a URL shortner.
Introduction
This is a project guide I'll suggest you to follow it step by step by yourself. In this article you will learn how to make a url shortner using Svelte, SvelteKit, Redis and TailwindCSS.
Initialization
We are going to start with sveltekit starter project. Then we gonna setup TailwindCSS (if you don't know how to read my Using TailwindCSS in SvelteKit to make a Design System : Part One).
- Required package other than above setup (npm i package-name)
1. redis
2. dotenv
- Create a
.env
file for your environment variables. In this project we gonna use Redis and I'm using Railway which is a PaaS provider for DB's.
// .env
REDIS_URL=redis://localhost:6379 // Use your own it's for example.
- Create a new file in
src
directory by the namehooks.ts
.
// hooks.ts
import dotenv from 'dotenv';
dotenv.config();
This is gonna load your environment variables in svelteKit.
Connecting server to Redis
Make sure you have installed it in your project if you haven't then run this command in your terminal
npm i redis
- Create a
lib
folder insrc
directory and inlib
folder create a fileredisConnection.ts
. In this file we going to handle ourRedis
connection and going to add some basic function toset
orget
values to/fromRedis
.
// redisConnection.ts
import { createClient } from 'redis';
import log from './log';
const client = createClient({ url: process.env.REDIS_URL as string });
let connectPromise: Promise<void> | undefined;
let errorOnce = true;
async function autoConnect(): Promise<void> {
if (!connectPromise) {
errorOnce = true;
connectPromise = new Promise((resolve, reject) => {
client.once('error', (err) => reject(new Error(`Redis: ${err.message}`)));
client.connect().then(resolve, reject);
});
}
await connectPromise;
}
client.on('error', (err) => {
if (errorOnce) {
log.error('Redis:', err);
errorOnce = false;
}
});
client.on('connect', () => {
log('Redis up');
});
client.on('disconnect', () => {
connectPromise = undefined;
log('Redis down');
});
async function get<T>(key: string): Promise<T | undefined>;
async function get<T>(key: string, fallback: T): Promise<T>;
async function get<T>(key: string, fallback?: T): Promise<T | undefined> {
await autoConnect();
const value = await client.get(key);
if (value === null) {
return fallback;
}
return JSON.parse(value);
}
async function set(
key: string,
value: unknown,
options?: { ttl: number } // TTL in seconds
): Promise<void> {
const data = JSON.stringify(value);
const config = options ? { EX: options.ttl } : {};
await autoConnect();
await client.set(key, data, config);
await client.publish(key, data);
}
const storage = {
get,
set
};
export default storage;
Here I'm using logger too, so skip logger for now. If you wanna learn how i did it check out Multiplayer Dice Game by bfanger on github. He is using
socket.io
,redis
and many other things, you gonna learn alot from this.
- Explaination
-- Create a client for redis
in node
. We gonna use createClient
from redis
. Which needs your REDIS_URL
that we added in .env
.
const client = createClient({ url: process.env.REDIS_URL as string });
-- We gonna add a autoConnect function which will connect to Redis
when we gonna set
or get
a value in redis.
let connectPromise: Promise<void> | undefined;
let errorOnce = true;
async function autoConnect(): Promise<void> {
if (!connectPromise) {
errorOnce = true;
connectPromise = new Promise((resolve, reject) => {
client.once('error', (err) => reject(new Error(`Redis: ${err.message}`)));
client.connect().then(resolve, reject);
});
}
await connectPromise;
}
-- Now we added three initiators which checks our connection with redis and respond according to that, e.g. error
, connected
and disconnected
.
client.on('error', (err) => {
if (errorOnce) {
log.error('Redis:', err);
errorOnce = false;
}
});
client.on('connect', () => {
log('Redis up');
});
client.on('disconnect', () => {
connectPromise = undefined;
log('Redis down');
});
-- We are now going to add our functional and need function to get data from redis.
async function get<T>(key: string): Promise<T | undefined>;
async function get<T>(key: string, fallback: T): Promise<T>;
async function get<T>(key: string, fallback?: T): Promise<T | undefined> {
await autoConnect();
const value = await client.get(key);
if (value === null) {
return fallback;
}
return JSON.parse(value);
}
Here we first defined the types of function. Function takes one parameter
key
which is required to find value inredis
. First we going to connect withredis
using our auto connect function then going to get the value using our providedkey
and then we going to return parsed JSON.
-- Now we gonna add our set function which going to help us in adding value to redis
. Set function takes two parameters key
and value
. Key is needed to be unique which help us to get the item from redis
.
async function set(
key: string,
value: unknown,
options?: { ttl: number } // TTL in seconds
): Promise<void> {
const data = JSON.stringify(value);
const config = options ? { EX: options.ttl } : {};
await autoConnect();
await client.set(key, data, config);
await client.publish(key, data);
}
-- Finally, we going to export them so we can use them anywhere in our project.
const storage = {
get,
set
};
export default storage;
That's all we need to add in redis to work in our project.
Adding Frontend and Shadow endpoints
Here we going to add our html and tailwindcss to make our input box and Buttons.
// index.svelte
<script lang="ts">
import { page } from '$app/stores';
import Clipboard from '$lib/Clipboard.svelte';
let url: string;
let isURLGenerated: boolean = false;
let isInvalidURL: boolean = false;
function isValidHttpUrl(string) {
let url;
try {
url = new URL(string);
} catch (_) {
return false;
}
return url.protocol === 'http:' || url.protocol === 'https:';
}
async function getURL() {
if (isValidHttpUrl(url)) {
const r = (Math.random() + 1).toString(36).substring(7);
const redirectURL = `${$page.url}${r}`;
isInvalidURL = false;
const data = { key: r, url: url };
await fetch('/', {
method: 'POST',
headers: {
accept: 'application/json'
},
body: JSON.stringify(data)
});
url = redirectURL;
isURLGenerated = true;
} else isInvalidURL = true;
}
</script>
<svelte:head>
<title>Shrink | Home</title>
</svelte:head>
<div class="bg-white w-screen h-screen flex flex-col justify-center text-center">
<h1 class="text-6xl p-4 text-fuchsia-500 font-bold">Shrink Me, Web.</h1>
<div class="flex justify-center w-full p-10">
<div class="mb-3 xl:w-2/4">
{#if isInvalidURL}
<div
id="alert-border-2"
class="flex p-4 mb-4 bg-red-100 border-t-4 border-red-500 dark:bg-red-200"
role="alert"
>
<svg
class="flex-shrink-0 w-5 h-5 text-red-700"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
><path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd"
/></svg
>
<div class="ml-3 text-sm font-medium text-red-700">You have typed wrong URL.</div>
</div>
{/if}
<div class="input-group relative flex items-stretch w-full mb-4">
<input
type="text"
class="form-control relative flex-auto block w-full border-b-2 px-3 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding transition ease-in-out m-0 focus:outline-none duration-300 focus:text-gray-700 focus:bg-white {isURLGenerated
? 'border-emerald-600 focus:border-emerald-600'
: 'border-fuchsia-600 focus:border-fuchsia-600'}"
placeholder="Paste or type your URL"
aria-label="url"
aria-describedby="url"
bind:value={url}
/>
<Clipboard
text={url}
let:copy
on:copy={() => {
console.log('Has Copied');
}}
>
{#if isURLGenerated}
<button
on:click={copy}
class="inline-block px-6 py-2 border-2 border-emerald-600 bg-emerald-600 text-white font-medium text-xs leading-tight uppercase hover:bg-white hover:text-emerald-600 transition duration-300 ease-in-out"
type="button"
id="button-copy">Copy</button
>
<button
on:click={() => {
url = '';
isURLGenerated = false;
}}
class="inline-block px-6 py-2 border-2 border-rose-600 bg-rose-600 text-white font-medium text-xs leading-tight uppercase hover:bg-white hover:text-rose-600 transition duration-300 ease-in-out"
type="button"
id="button-reset">Reset</button
>
{:else}
<button
on:click={getURL}
class="inline-block px-6 py-2 border-2 border-fuchsia-600 bg-fuchsia-600 text-white font-medium text-xs leading-tight uppercase hover:bg-white hover:text-fuchsia-600 transition duration-300 ease-in-out"
type="button"
id="button-addon3">Shrink</button
>
{/if}
</Clipboard>
</div>
<div class="flex justify-center items-center">
<div class="spinner-grow inline-block w-8 h-8 bg-fuchsia-600 rounded-full opacity-0" />
</div>
</div>
</div>
</div>
- Explanation Here, I'm going to explain the basic functionality and request making to shadow endpoint. Please understand the html and tailwind part from my earlier posts.
-- First we going to focus on pur script
tag.
<script lang="ts">
import { page } from '$app/stores';
import Clipboard from '$lib/Clipboard.svelte';
let url: string;
let isURLGenerated: boolean = false;
let isInvalidURL: boolean = false;
function isValidHttpUrl(string) {
let url;
try {
url = new URL(string);
} catch (_) {
return false;
}
return url.protocol === 'http:' || url.protocol === 'https:';
}
async function getURL() {
if (isValidHttpUrl(url)) {
const r = (Math.random() + 1).toString(36).substring(7);
const redirectURL = `${$page.url}${r}`;
isInvalidURL = false;
const data = { key: r, url: url };
await fetch('/', {
method: 'POST',
headers: {
accept: 'application/json'
},
body: JSON.stringify(data)
});
url = redirectURL;
isURLGenerated = true;
} else isInvalidURL = true;
}
</script>
Here, I have to define are parameters. Those are URL : which is provided by the user, isURLGenerated : checking if url generated or not (we need it to change are buttons from generate to copy), isInvalidURL : is defined for html to activate alert if url is not valid.
isValidHttpUrl
: This function help to verify our URL which is provided by user is a valid URL or not.
getURL
: This function generates are short url. First, I have added check forisValidHttpUrl
if this is valid we gonna proceed otherwise we gonna returnisInvalidURL = true
. If URL is valid then going to generate a random string and after that we going to make a request to our shadow endpoint/api to save ourkey
generated string andurl
provided by the user.
- Shadow Endpoint or API
// index.ts
import storage from '$lib/redisConnection';
import type { RequestHandler } from '@sveltejs/kit';
export const POST: RequestHandler = async ({ request }) => {
const data = await request.json();
await storage.set(data.key, data.url);
return {
body: {
status: 200,
error: null
}
};
};
-- Here I have added our POST
method handling which takes a request parameter from which we get the data we provide while making request from index.svelte
. Here we save that data to redis using set function.
This is all we need to generate a valid short URL and save in redis. Now we gonna how we redirect user to URL when visit using short URL generated by you
.
Getting original URL and Redirecting to the URL
In this section we are going to learn how we get parameter from URL and getting data from redis and then redirecting to original URL.
-- Add a new file in routes
directory [slug].ts
and add following lines of code
// [slug].ts
import storage from '$lib/redisConnection';
import type { RequestHandler } from '@sveltejs/kit';
export const GET: RequestHandler = async ({ params }) => {
if (params.slug.length > 3) {
return {
headers: {
Location: await storage.get(params.slug)
},
status: 302
};
} else
return {
headers: {
Location: '/'
},
status: 302,
error: new Error('Short URL doesn't exist')
};
};
Here we have added a
GET
method which will be called when we gonna visit any short url example. We are using {params} which is an inbuilt dictionary which contains our slug of the page.I have added condition to check length of the slug(generated string in
index.svelte
) and using that we gonna query our redis using get function and going to get the value which is our saved original URL. I have addedLocation
in headers which helps us to redirect to that URL and added a status or redirect. If condition fails its going to redirected to home of our URL Shortner.
That how we gonna get and redirect to the original URL.
That is the end of the article. You should check all the mentioned resources and links which going to help you understand this post much better.
This is me writing for you. If you wanna ask or suggest anything please put it in comment.
Top comments (1)
Nicee post!!