docker run --name redis -d -p 6379:6379 redis
npx create-next-app@latest
Need to install the following packages:
create-next-app@14.1.4
Ok to proceed? (y) Y
What is your project named? Project-name
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No /Yes
Would you like to use App Router? (recommended) No /Yes
Would you like to customize the default import alias (@/*)?No / Yes
npm install --save ioredis geist
.env
file. We will follow that rule and we will create .env
at root of our project with following codeNEXT_PUBLIC_BASE_URL="http://localhost:3000"
REDIS_URL="redis://default:@localhost:6379"
.env
file holds secrets, it is important to add it to .gitignore
..env
npm run dev
src/app/layout.tsx
import {Inter} from 'next/font/google';
to import GeistSans fontimport {GeistSans} from 'geist/font/sans';
const inter = Inter({subsets: ['latin']});
<body className={Inter.className}>{children}</body>
<body className={GeistSans.className}>{children}</body>
src/app/layout.tsx
should look like thisimport type { Metadata } from "next";
import { GeistSans } from "geist/font/sans";
import "./globals.css";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={GeistSans.className}>{children}</body>
</html>
);
}
src/lib
folder in which we'll create redis.ts
file.import Redis from "ioredis";
export default class RedisLib {
private static instance: RedisLib;
private redisClient: Redis;
private constructor() {
this.redisClient = new Redis(process.env.REDIS_URL as string);
}
public static getInstance(): RedisLib {
if (!this.instance) {
if (!process.env.REDIS_URL) {
throw new Error("REDIS_URL is not defined");
}
this.instance = new RedisLib();
}
return this.instance;
}
public getClient(): Redis {
return this.redisClient;
}
}
src/app/error.tsx
in which we'll create Error component'use client'
import { useEffect } from 'react'
export default function Error({
error,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error(error)
}, [error])
return (
<div className="w-full h-[90vh] flex flex-col justify-center items-center gap-10">
<h2 className="text-3xl font-bold">Something went wrong!</h2>
<button className="text-xl bg-blue-400 rounded-xl px-4 py-2 text-black hover:bg-blue-500" onClick={() => window.location.reload()}>
Try again
</button>
<button className="text-xl bg-red-400 rounded-xl px-4 py-2 text-black hover:bg-red-500" onClick={() => window.history.back()}>
Go Back
</button>
<div className="flex flex-col justify-center items-center gap-1">
<code>Name: {error.name}</code>
<code>Message: {error.message}</code>
</div>
</div>
)
}
/app/page.tsx
and paste in following codeexport default function Home() {
return(<h1>Hello World</h1>)
}
Hello World
message .... Good job, you're smart cookie<h1>Hello World</h1>
and create simple div with header, input field and button. Code should be something like this.<main className="w-full h-[95vh] flex justify-center items-center">
<div className=" flex flex-col items-center w-1/4 h-fit outline-1 outline outline-gray-500 bg-gray-900 rounded-lg gap-10 py-5 px-3">
<h1 className="text-3xl text-white font-bold" > ShortURL </h1>
<input placeholder="Enter URL to shorten" className="w-full h-10 px-3 rounded-lg bg-slate-600 text-white" />
<button className="text-xl bg-blue-400 rounded-xl px-4 py-2 text-black hover:bg-blue-700 hover:text-white ease-in-out duration-300">
Create URL
</button>
</div>
</main>
Submit
into Home
page component. Just like this.export default function Home() {
async function Submit() {}
return (
<main className="w-full h-[95vh] flex justify-center items-center">
<div className=" flex flex-col items-center w-1/4 h-fit outline-1 outline outline-gray-500 bg-gray-900 rounded-lg gap-10 py-5 px-3">
<h1 className="text-3xl text-white font-bold" > ShortURL </h1>
<input placeholder="Enter URL to shorten" className="w-full h-10 px-3 rounded-lg bg-slate-600 text-white" />
<button className="text-xl bg-blue-400 rounded-xl px-4 py-2 text-black hover:bg-blue-700 hover:text-white ease-in-out duration-300">
Create URL
</button>
</div>
</main>
);
}
onClick
event, but by doing so we need to switch into client side rendering by adding 'use client'
flag at the start and now we can link the onClick
'use client'
export default function Home() {
async function Submit() {}
return (
<main className="w-full h-[95vh] flex justify-center items-center">
<div className=" flex flex-col items-center w-1/4 h-fit outline-1 outline outline-gray-500 bg-gray-900 rounded-lg gap-10 py-5 px-3">
<h1 className="text-3xl text-white font-bold" > ShortURL </h1>
<input placeholder="Enter URL to shorten" className="w-full h-10 px-3 rounded-lg bg-slate-600 text-white" />
<button className="text-xl bg-blue-400 rounded-xl px-4 py-2 text-black hover:bg-blue-700 hover:text-white ease-in-out duration-300" onClick={() => Submit()}>
Create URL
</button>
</div>
</main>
);
}
useRef
hook, but first we need to import it from react
. Then create instance and link it to the input field. Code should look like this'use client'
import {useRef} from 'react';
export default function Home() {
const inputRef = useRef(null);
async function Submit() {}
return (
<main className="w-full h-[95vh] flex justify-center items-center">
<div className=" flex flex-col items-center w-1/4 h-fit outline-1 outline outline-gray-500 bg-gray-900 rounded-lg gap-10 py-5 px-3">
<h1 className="text-3xl text-white font-bold" > ShortURL </h1>
<input ref={inputRef} placeholder="Enter URL to shorten" className="w-full h-10 px-3 rounded-lg bg-slate-600 text-white" />
<button className="text-xl bg-blue-400 rounded-xl px-4 py-2 text-black hover:bg-blue-700 hover:text-white ease-in-out duration-300" onClick={() => Submit()}>
Create URL
</button>
</div>
</main>
);
}
Submit()
into simple alert function with use of regex to check if the url is valid. By doing this:'use client'
import {useRef} from 'react';
export default function Home() {
const inputRef = useRef<HTMLInputElement>(null);
async function Submit() {
//regex pattern
const _pattern = "^(https?:\\/\\/)[\\da-z\\.-]+\\.[a-z]{2,}([\\/\\w .-]*)(:[\\w]+)?(\\?[\\w=&]+)?(#\\w+)?\\/?$";
//initializing regex
const _regex = new RegExp(_pattern);
// get value of input field
const urlInput = inputRef.current?.value || null
if (!urlInput) return;
if (!_regex.test(urlInput)) {
alert("Invalid URL");
return;
} else {
alert("Valid URL");
return;
}
}
return (
<main className="w-full h-[95vh] flex justify-center items-center">
<div className=" flex flex-col items-center w-1/4 h-fit outline-1 outline outline-gray-500 bg-gray-900 rounded-lg gap-10 py-5 px-3">
<h1 className="text-3xl text-white font-bold" > ShortURL </h1>
<input ref={inputRef} placeholder="Enter URL to shorten" className="w-full h-10 px-3 rounded-lg bg-slate-600 text-white" />
<button className="text-xl bg-blue-400 rounded-xl px-4 py-2 text-black hover:bg-blue-700 hover:text-white ease-in-out duration-300" onClick={() => Submit()}>
Create URL
</button>
</div>
</main>
);
}
src/api
in which we create a folder with the name of the api route that we want. For this example we are going to create a folder named src/api/url
. In this folder we need to create a file route.ts
with basic GET, POST methods. Such as this:export async function GET(req: Request) {
return new Response("Hello, Next.js! GET");
}
export async function POST(req: Request) {
return new Response("Hello, Next.js! POST");
}
src/app/page.tsx
by replacing alert("Valid URL");
with this:// post request header
const headers = {
method: 'POST',
headers: {
accept: 'application/json',
},
body: JSON.stringify({url: urlInput}),
}
// Fetch call with headers
const response = await fetch(process.env.NEXT_PUBLIC_BASE_URL+'/api/url', headers)
// Check if response code isn't 200
if (!response.ok) {
alert('Failed to fetch data')
return
}
// json data from api
const data = await response.json()
alert(process.env.NEXT_PUBLIC_BASE_URL + "/" + data.id)
src/app/page.tsx
should look like this:'use client'
import {useRef} from 'react';
export default function Home() {
const inputRef = useRef<HTMLInputElement>(null);
async function Submit() {
//regex pattern
const _pattern = "^(https?:\\/\\/)[\\da-z\\.-]+\\.[a-z]{2,}([\\/\\w .-]*)(:[\\w]+)?(\\?[\\w=&]+)?(#\\w+)?\\/?$";
//initializing regex
const _regex = new RegExp(_pattern);
// get value of input field
const urlInput = inputRef.current?.value || null
if (!urlInput) return;
if (!_regex.test(urlInput)) {
alert("Invalid URL");
return;
} else {
// post request header
const headers = {
method: 'POST',
headers: {
accept: 'application/json',
},
body: JSON.stringify({url: urlInput}),
}
// Fetch call with headers
const response = await fetch(process.env.NEXT_PUBLIC_BASE_URL+'/api/url', headers)
// Check if response code isn't 200
if (!response.ok) {
alert('Failed to fetch data')
return
}
// json data from api
const data = await response.json()
alert(process.env.NEXT_PUBLIC_BASE_URL + "/" + data.id)
return;
}
}
return (
<main className="w-full h-[95vh] flex justify-center items-center">
<div className=" flex flex-col items-center w-1/4 h-fit outline-1 outline outline-gray-500 bg-gray-900 rounded-lg gap-10 py-5 px-3">
<h1 className="text-3xl text-white font-bold" > ShortURL </h1>
<input ref={inputRef} placeholder="Enter URL to shorten" className="w-full h-10 px-3 rounded-lg bg-slate-600 text-white" />
<button className="text-xl bg-blue-400 rounded-xl px-4 py-2 text-black hover:bg-blue-700 hover:text-white ease-in-out duration-300" onClick={() => Submit()}>
Create URL
</button>
</div>
</main>
);
}
function generateId() {
const min = 1;
const max = 1099511637775;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function generateId() {
const min = 1;
const max = 1099511637775;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export async function GET(req: Request) {
return new Response("Hello, Next.js! GET");
}
export async function POST(req: Request) {
try {
const data = await req.json()
const { url } = data
let id = generateId().toString(16)
return new Response(JSON.stringify({id: id}), { status: 200 })
} catch (error) {
return new Response('Error', { status: 500 })
}
}
RedisLib
that we've created in the begining. To use it we need to import it and then get Redis instance.import RedisLib from '@/lib/redis'
function generateId() {
const min = 1;
const max = 1099511637775;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export async function GET(req: Request) {
return new Response("Hello, Next.js! GET");
}
export async function POST(req: Request) {
try {
const data = await req.json()
const { url } = data
const redis = RedisLib.getInstance().getClient();
let id = generateId().toString(16)
return new Response(JSON.stringify({id: id}), { status: 200 })
} catch (error) {
return new Response('Error', { status: 500 })
}
}
import RedisLib from '@/lib/redis'
function generateId(): number {
const min = 1;
const max = 1099511627775
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export async function GET(req: Request) {
return new Response("Hello, Next.js! GET");
}
export async function POST(req: Request) {
try {
const data = await req.json()
const { url } = data
const redis = RedisLib.getInstance().getClient();
let id = await redis.get(url);
if (id) {
return new Response(JSON.stringify({id: id}), { status: 200 })
} else {
let id = generateId().toString(16)
const time = 60*60*4;
await redis.set(id, url, 'EX', time)
await redis.set(url, id, 'EX', time)
return new Response(JSON.stringify({id: id}), { status: 200 })
}
} catch (error) {
return new Response('Error', { status: 500 })
}
}
[id]
and [slug]
, where id is for number and slug is for string. For this case we are going to use [slug]
, because we have a hex number.[slug]
route is very simple, we just need to add it to src/app/[slug]
folder. In it we need to create page.tsx
file with basic code just like this: export default function Page({ params }: { params: { slug: string } }) {
return <h1>Dynamic page : {params.slug}</h1>;
}
src/app/[slug]/error.tsx
. For that we can reuse code from src/app/error.tsx
'use client'
import { useEffect } from 'react'
export default function Error({
error,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error(error)
}, [error])
return (
<div className="w-full h-[90vh] flex flex-col justify-center items-center gap-10">
<h2 className="text-3xl font-bold">Something went wrong!</h2>
<button className="text-xl bg-blue-400 rounded-xl px-4 py-2 text-black hover:bg-blue-500" onClick={() => window.location.reload()}>
Try again
</button>
<button className="text-xl bg-red-400 rounded-xl px-4 py-2 text-black hover:bg-red-500" onClick={() => window.history.back()}>
Go Back
</button>
<div className="flex flex-col justify-center items-center gap-1">
<code>Name: {error.name}</code>
<code>Message: {error.message}</code>
</div>
</div>
)
}
useEffect
hook and create a simple callback function. Firstly to use any React hooks we need to switch into client component and then import useEffect
hook.'use client'
import { useEffect } from 'react';
export default function Page({ params }: { params: { slug: string } }) {
useEffect(() => {},[])
return <h1>Dynamic page : {params.slug}</h1>;
}
api/url
path, so we don't have to create a separate api route. The first step is to create headers for our request, then assemble an url with parameter id and fetch it.Then we need to check if we got any errors. If we did not, we will redirect the user to the url.'use client'
import { useEffect } from "react";
export default function Page({ params }: { params: { slug: string } }) {
async function GetUrl() {
const headers = {
method: "GET",
headers: {
accept: "application/json",
},
};
const response = await fetch(
process.env.NEXT_PUBLIC_BASE_URL + "/api/url?id=" + params.slug,
headers
);
if (!response.ok) {
window.location.replace(process.env.NEXT_PUBLIC_BASE_URL || '')
}
const data = await response.json();
window.location.replace(data.url);
return;
}
useEffect(() => {}, []);
return <h1>Dynamic page :{params.slug}</h1>;
}
'use client'
import { useEffect } from "react";
export default function Page({ params }: { params: { slug: string } }) {
async function GetUrl() {
const headers = {
method: "GET",
headers: {
accept: "application/json",
},
};
const response = await fetch(
process.env.NEXT_PUBLIC_BASE_URL + "/api/url?id=" + params.slug,
headers
);
if (!response.ok) {
window.location.replace(process.env.NEXT_PUBLIC_BASE_URL || '')
}
const data = await response.json();
window.location.replace(data.url);
return;
}
useEffect(() => {GetUrl()}, [GetUrl]);
return <h1>Dynamic page :{params.slug}</h1>;
}
src/api/url/route.ts
by firtly parsing the url to get the id parameter.import RedisLib from '@/lib/redis'
function generateId(): number {
const min = 1;
const max = 1099511627775
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export async function GET(req: Request) {
try {
const path = new URL(req.url)
const id = path.searchParams.get('id')
if (!id) throw new Error('No id provided')
return new Response(JSON.stringify({id: id}), { status: 200 })
} catch (error) {
return new Response('Error', { status: 500 })
}
}
export async function POST(req: Request) {
try {
const data = await req.json()
const { url } = data
const redis = RedisLib.getInstance().getClient();
let id = await redis.get(url);
if (id) {
return new Response(JSON.stringify({id: id}), { status: 200 })
} else {
let id = generateId().toString(16)
const time = 60*60*4;
await redis.set(id, url, 'EX', time)
await redis.set(url, id, 'EX', time)
return new Response(JSON.stringify({id: id}), { status: 200 })
}
} catch (error) {
return new Response('Error', { status: 500 })
}
}
RedisLib
and check if we have id
in cache. Based on that we will return the url or throw 404 error codeimport RedisLib from '@/lib/redis'
function getRandomNumber(): number {
const min = 1;
const max = 1099511627775
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export async function GET(req: Request){
try {
const path = new URL(req.url)
const id = path.searchParams.get('id')
if (!id) throw new Error('No id provided')
const redis = RedisLib.getInstance().getClient();
let url = await redis.get(id);
if (id) {
return new Response(JSON.stringify({url: url}), { status: 200 })
} else {
return new Response(JSON.stringify({message: 'No url found'}), { status: 404 })
}
} catch (error) {
return new Response('Error', { status: 500 })
}
}
export async function POST(req: Request) {
try {
const data = await req.json()
const { url } = data
const redis = RedisLib.getInstance().getClient();
let id = await redis.get(url);
if (id) {
return new Response(JSON.stringify({id: id}), { status: 200 })
} else {
let id = getRandomNumber().toString(16)
await redis.set(id, url, 'EX', 60 * 60 * 4)
await redis.set(url, id, 'EX', 60 * 60 * 4)
return new Response(JSON.stringify({id: id}), { status: 200 })
}
} catch (error) {
return new Response('Error', { status: 500 })
}
}
export const dynamic = 'force-dynamic'
import RedisLib from '@/lib/redis'
export const dynamic = 'force-dynamic'
function getRandomNumber(): number {
const min = 1;
const max = 1099511627775
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export async function GET(req: Request){
try {
const path = new URL(req.url)
const id = path.searchParams.get('id')
if (!id) throw new Error('No id provided')
const redis = RedisLib.getInstance().getClient();
let url = await redis.get(id);
if (id) {
return new Response(JSON.stringify({url: url}), { status: 200 })
} else {
return new Response(JSON.stringify({message: 'No url found'}), { status: 404 })
}
} catch (error) {
return new Response('Error', { status: 500 })
}
}
export async function POST(req: Request) {
try {
const data = await req.json()
const { url } = data
const redis = RedisLib.getInstance().getClient();
let id = await redis.get(url);
if (id) {
return new Response(JSON.stringify({id: id}), { status: 200 })
} else {
let id = getRandomNumber().toString(16)
await redis.set(id, url, 'EX', 60 * 60 * 4)
await redis.set(url, id, 'EX', 60 * 60 * 4)
return new Response(JSON.stringify({id: id}), { status: 200 })
}
} catch (error) {
return new Response('Error', { status: 500 })
}
}
Chapter 1
Prequisites
Chapter 2
Installation
Chapter 3
Basic Configuration
Chapter 4
Home page
Chapter 5
Generating ID for url
Chapter 6
Dynamic path for lookup and lookup API