Building a User-Friendly URL Shortener Using Spring Boot, Postgres, and FL0
TL;DR
In this tutorial, we will create a user-friendly URL shortener using SpringBoot and Postgres, deployed on FL0 for easy management and control. 🔗✨
Introduction
Nowadays, URLs have become an essential part of our workflow. However, long and complex URLs can be difficult to share or work with.
In this tutorial, we will build a simple-to-use URL shortener that provides a convenient way to transform long URLs into shorter, more manageable links in just one click! 🧑‍💻
We would be using Spring Boot
to build our backend, Postgres
as our database, both deployed simply using FL0. Then we would go ahead and build the user interface in the form of a simple Chrome Extension, which could call our APIs!
Here’s some internet humor before we get started:
Overview
Before we dive into the code, let’s take a moment to understand the high-level overview of our project.
Our URL shortener will be a Chrome extension designed to provide a seamless and delightful experience for users. Let’s walk through the user journey to get a clear picture:
- Navigation: The user opens Chrome and visits the webpage he/she wants to shorten using our installed extension.
- URL Shortening: Upon clicking the extension, it fetches the active tab’s URL and sends it to our shortening service. This service is a Spring Boot application hosted on FL0, which processes the URL and generates a unique path.
- URL Construction and Copying: The extension takes this unique path, constructs the full shortened URL on the front end, displays it to the user, and it can be copied with a single click.
Here’s a high-level diagram for better understanding:
Step 0: Setting Up the Spring Boot Project
Before we begin, we need to make sure we have the following tools installed:
- Code Editor: In this tutorial, we’ll be using IntelliJ IDEA.
- JDK 17: we need to have JDK 17 installed.
- Docker Desktop (on Mac/Windows): We’ll utilize Docker to containerize our application and simplify deployment.
Now we would go ahead and set up our new Spring Boot project using Spring Initializr 🌱
- Let’s visit start.spring.io. This is the Spring Initializr, a web-based tool that helps in creating Spring Boot projects quickly.
- Now, we would need to configure our settings as follows:
Project Metadata: Specify the project metadata such as group, artifact, and version.
Dependencies: Add the following dependencies:
Spring Web
: To build RESTful APIs and handle HTTP requests.Spring Data JPA
: For working with databases using Object-Relational Mapping (ORM).PostgreSQL Driver
: To connect our application with a PostgreSQL database.Lombok
: A library that simplifies Java code by reducing boilerplate.
3. We will select the latest stable Java version (Java 17), choose the project packaging as JAR, and select Gradle
as the build tool. 🧑‍💻
4. Click on the “Generate” button to download a zip file containing the project structure and necessary files 🗂️ as shown👇
Setting Up the Project
- We will extract the downloaded zip file to a preferred location.
- Now we will open our code editor (IntelliJ IDEA, in our case) and import the project by selecting the extracted folder as the project directory.
- Once the project is loaded, we would need to download the project dependencies specified in the
build.gradle
file. This can be done automatically in IntelliJ IDEA by clicking on the Gradle toolbar located on the right side of the editor and selecting the "Refresh" button. - Verify that the dependencies are successfully downloaded by checking the Gradle Console for any errors or warnings.
Step 1: Database and Config
In this step, we’ll configure the necessary files and set up the database connection for our user-friendly URL shortener. Let’s get started with the configuration setup!
Postgresql Database Setup
To run our app locally, we would also need postgresql
running. So, lets set it up quickly. We create a new folder src/main/resources/local
and create a new file local-postgre-docker-compose.yml
version: '3'
services:
postgres:
image: postgres
restart: always
ports:
- 5432:5432
environment:
POSTGRES_DB: url_shortener
POSTGRES_USER: user123
POSTGRES_PASSWORD: pass123
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
To start our DB, in terminal navigate to the folder containing this file and run the command:
docker-compose -f local-postgre-docker-compose.yml up -d
This will start postgresql
database as a docker container
and we will be able to connect to it on localhost:5432
using the credentials as mentioned in the above docker file
Configuring Files and Database Connection
- Open the
application.yml
file located in thesrc/main/resources
directory. This file allows us to define properties for our application. - Add the following properties:
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://${DB_HOST_NAME:localhost}:${DB_PORT:5432}/${DB_NAME:url_shortener}
username: ${DB_USERNAME:user123}
password: ${DB_PASSWORD:pass123}
jpa:
hibernate:
ddl-auto: update
short-url:
allowed-characters: ${ALLOWED_CHARS:abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789}
key-length: ${KEY_LENGTH:6}
- The variables written as
${ENV_VARIABLE_NAME:default_value}
can be set using environment variables while deployment. - For local development we can use the default values.
server.port
: The port on which our application will run.spring.datasource.url
: The URL for connecting to our PostgreSQL database.spring.datasource.username
andspring.datasource.password
: PostgreSQL database credentials.short-url.allowed-characters
: The characters allowed in the generated keys. Feel free to modify or expand the character set if desired.short-url.key-length
: The length of the generated keys for short url. We will be using length of 6 characters as key.
Now we can run our application using below command:
./gradlew bootRun
ShortUrlConfig Class
To centralize our configuration properties and make them easily accessible, let’s create a ShortUrlConfig
class. This class will be annotated with @ConfigurationProperties(prefix="short-url")
to bind the properties from the application.yml
file to the corresponding fields in our class. Here's an example:
package com.fl0.urlshortener.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "short-url")
@Getter
@Setter
public class ShortUrlConfig {
private String allowedCharacters;
private int keyLength;
}
This will require us to add @ConfigurationPropertiesScan
on top of the main application class. So, lets add it to our UrlshortenerApplication
:
package com.fl0.urlshortener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication
**@ConfigurationPropertiesScan**
public class UrlshortenerApplication {
public static void main(String[] args) {
SpringApplication.run(UrlshortenerApplication.class, args);
}
}
Now that we have our files configured and the database connection established, it’s time to move on to creating the necessary models and repositories.
In this step, we’ll configure the necessary files and set up the database connection for our user-friendly URL shortener. Let’s get started with the configuration setup!
Postgresql Database Setup
To run our app locally, we would also need postgresql
running. So, lets set it up quickly. We create a new folder src/main/resources/local
and create a new file local-postgre-docker-compose.yml
version: '3'
services:
postgres:
image: postgres
restart: always
ports:
- 5432:5432
environment:
POSTGRES_DB: url_shortener
POSTGRES_USER: user123
POSTGRES_PASSWORD: pass123
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
To start our DB, in terminal navigate to the folder containing this file and run the command:
docker-compose -f local-postgre-docker-compose.yml up -d
This will start postgresql
database as a docker container
and we will be able to connect to it on localhost:5432
using the credentials as mentioned in the above docker file
Configuring Files and Database Connection
- Open the
application.yml
file located in thesrc/main/resources
directory. This file allows us to define properties for our application. - Add the following properties:
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://${DB_HOST_NAME:localhost}:${DB_PORT:5432}/${DB_NAME:url_shortener}
username: ${DB_USERNAME:user123}
password: ${DB_PASSWORD:pass123}
jpa:
hibernate:
ddl-auto: update
short-url:
allowed-characters: ${ALLOWED_CHARS:abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789}
key-length: ${KEY_LENGTH:6}
- The variables written as
${ENV_VARIABLE_NAME:default_value}
can be set using environment variables while deployment. If not set then the default value will be used. - For local development we can use the default values.
server.port
: The port on which our application will run.spring.datasource.url
: The URL for connecting to our PostgreSQL database.spring.datasource.username
andspring.datasource.password
: PostgreSQL database credentials.short-url.allowed-characters
: The characters allowed in the generated keys. Feel free to modify or expand the character set if desired.short-url.key-length
: The length of the generated keys for short url. We will be using length of 6 characters as key.
Now we can run our application using below command:
./gradlew bootRun
ShortUrlConfig Class
To centralize our configuration properties and make them easily accessible, let’s create a ShortUrlConfig
class. This class will be annotated with @ConfigurationProperties(prefix="short-url")
to bind the properties from the application.yml
file to the corresponding fields in our class. Here's an example:
package com.fl0.urlshortener.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "short-url")
@Getter
@Setter
public class ShortUrlConfig {
private String allowedCharacters;
private int keyLength;
}
This will require us to add @ConfigurationPropertiesScan
on top of the main application class. So, lets add it to our UrlshortenerApplication
:
package com.fl0.urlshortener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication
**@ConfigurationPropertiesScan**
public class UrlshortenerApplication {
public static void main(String[] args) {
SpringApplication.run(UrlshortenerApplication.class, args);
}
}
Now that we have our files configured and the database connection established, it’s time to move on to creating the necessary models and repositories.
Step 2: Creating Entity and Repository
Next we’ll define and create the necessary models and repositories for our URL shortener. The models will represent the table structure of our shortened URLs, and the repositories will handle the database operations. Let’s dive in!
ShortUrl Entity
- Create a new Java class named
ShortUrlEntity
in a new packagecom.fl0.urlshortener.entity
- Define the fields for the
ShortUrlEntity
entity class:
package com.fl0.urlshortener.entity;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "urls")
public class ShortUrlEntity {
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String key;
@Column(nullable = false, columnDefinition = "TEXT")
private String fullUrl;
@Column(nullable = false)
private Long clickCount;
}
- The
ShortUrlEntity
entity represents a shortened URL and is annotated with@Entity
to indicate that it's a JPA entity. The@Table
annotation specifies the name of the table in the database. - The entity has the following fields:
id
: The primary key generated automatically by the database.key
: The unique key representing the shortened URL.fullUrl
: The original full URL that was shortened.clickCount
: The number of times the shortened URL has been clicked.
ShortUrlRepository
- Create a new Java interface named
ShortUrlRepository
in a new packagecom.fl0.urlshortener.repository
- Extend the
JpaRepository
interface and define custom methods for the repository:
package com.fl0.urlshortener.repository;
import com.fl0.urlshortener.entity.ShortUrlEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ShortUrlRepository extends JpaRepository<ShortUrlEntity, Long> {
ShortUrlEntity findByKey(String key);
ShortUrlEntity findByFullUrl(String fullUrl);
}
- The
ShortUrlRepository
extends theJpaRepository
interface provided by Spring Data JPA. It enables us to perform CRUD operations on theShortUrlEntity
easily. - We will also define two custom methods:
findByKey
: Retrieves aShortUrlEntity
entity based on the given key.findByFullUrl
: Retrieves aShortUrlEntity
entity based on the given full URL.
These methods will be used in our service layer to retrieve and manipulate data.
Step 3: Creating DTOs, Utility and Service classes
Now we’ll create the data transfer objects (DTOs), a utility class and service layer. The DTOs will facilitate data transfer, the utility class will provide helpful methods and the service will handle the business logic. Let’s proceed!
DTOs (Data Transfer Objects)
- Let’s create a new Java class named
ShortUrlRequest
in thecom.example.urlshortener.dto
package. - Now we will define the fields for the
ShortUrlRequest
class:
package com.fl0.urlshortener.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ShortUrlRequest {
private String url;
}
- The
ShortUrlRequest
DTO represents a request to create a shortened URL. It has a single field,url
. - We will create another Java class named
ShortUrlResponse
in the same package. - Now we will define the fields for the
ShortUrlResponse
class:
package com.fl0.urlshortener.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class ShortUrlResponse {
private String key;
}
- The
ShortUrlResponse
DTO represents the response after creating a shortened URL. It has a single field:key
, which holds the unique key as a response.
ShortUrlUtil
We will create a new Java class named ShortUrlUtil
in the com.fl0.urlshortener.util
package and add the @Component
annotation to the ShortUrlUtil
class to make it a Spring bean:
package com.fl0.urlshortener.util;
import com.fl0.urlshortener.config.ShortUrlConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Random;
@Component
public class ShortUrlUtil {
private final ShortUrlConfig config;
@Autowired
public ShortUrlUtil(ShortUrlConfig config) {
this.config = config;
}
public String generateUniqueKey() {
int keyLength = config.getKeyLength();
String allowedCharacters = config.getAllowedCharacters();
StringBuilder keyBuilder = new StringBuilder();
Random random = new Random();
for (int i = 0; i < keyLength; i++) {
int randomIndex = random.nextInt(allowedCharacters.length());
keyBuilder.append(allowedCharacters.charAt(randomIndex));
}
return keyBuilder.toString();
}
}
The ShortUrlUtil
class is annotated with @Component
to make it a Spring bean. It is also injected with the ShortUrlConfig
using constructor injection.
The ShortUrlUtil
class provides a single method:
generateUniqueKey()
: Generates a unique key based on the specified length and allowed characters from the configuration.
This method will be used in the service layer.
UrlShortenerService
- Create a new Java class named
UrlShortenerService
in thecom.fl0.urlshortener.service
package. - Add the following methods to handle URL shortening and retrieval:
package com.fl0.urlshortener.service;
import com.fl0.urlshortener.dto.ShortUrlRequest;
import com.fl0.urlshortener.dto.ShortUrlResponse;
import com.fl0.urlshortener.entity.ShortUrlEntity;
import com.fl0.urlshortener.repository.ShortUrlRepository;
import com.fl0.urlshortener.util.ShortUrlUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.view.RedirectView;
@Service
@RequiredArgsConstructor
public class UrlShortenerService {
private final ShortUrlRepository repository;
private final ShortUrlUtil util;
public ShortUrlResponse createShortUrl(ShortUrlRequest request) {
String fullUrl = request.getUrl();
ShortUrlEntity existingShortUrl = repository.findByFullUrl(fullUrl);
if (existingShortUrl != null) {
return ShortUrlResponse.builder().key(existingShortUrl.getKey()).build();
} else {
String newKey = util.generateUniqueKey();
ShortUrlEntity newEntity = ShortUrlEntity.builder()
.key(newKey).fullUrl(fullUrl).clickCount(0L)
.build();
repository.save(newEntity);
return ShortUrlResponse.builder().key(newKey).build();
}
}
public RedirectView getFullUrl(String key) {
ShortUrlEntity entityInDb = repository.findByKey(key);
entityInDb.setClickCount(entityInDb.getClickCount() + 1);
repository.save(entityInDb);
return new RedirectView(entityInDb.getFullUrl());
}
}
The UrlShortenerService
class handles the business logic of URL shortening and retrieval. It relies on the ShortUrlRepository
for database operations and the ShortUrlUtil
for generating unique keys.
The createShortUrl
method checks if the URL already exists in the database.
If it exists, we get the key (the path) of the URL from the database.
Otherwise, it generates a new key, saves it along with the URL in the database, and returns the newly generated key.
The getFullUrl
method retrieves the original full URL based on the provided key.
It finds the key in the database, increments the click count, saves the changes, and redirects to the original full URL.
Our application is now equipped with the necessary components to handle the creation and retrieval of shortened URLs.
Step 5: Enabling Cross-Origin Resource Sharing (CORS)
To allow requests from the Chrome extension to our backend API, we need to configure Cross-Origin Resource Sharing (CORS). In this step, we’ll create a CorsConfig
class in our Spring Boot application to handle CORS configuration.
- We create a new Java class named
CorsConfig
in thecom.fl0.urlshortener.config
package.
package com.fl0.urlshortener.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST");
}
}
- The
CorsConfig
class implements theWebMvcConfigurer
interface, allowing us to customize the CORS configuration for our application. - With this configuration, our backend will allow requests from the Chrome extension, enabling communication between the extension and the API.
- Save the
CorsConfig
class.
We have successfully built the CorsConfig
class to handle CORS configuration in our Spring Boot application. This will ensure that our backend API can accept requests from our Chrome extension without any CORS-related issues. âś…
Next, let’s move on to build our Chrome extension. 👨🏻‍💻
Step 6: Building the Chrome Extension
In this step, we’ll create a custom Chrome extension for our URL shortener. The extension will provide a convenient way for users to shorten URLs directly from their browser. Let’s dive into the world of Chrome extension development!
- We will create a new project named
url-shortener-extension
.
manifest.json
{
"manifest_version": 3,
"name": "URL Shortener",
"version": "1.0",
"description": "Shortens URLs with ease!",
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_icon": "icon.png"
},
"permissions": ["activeTab"],
"icons": {
"16": "icon.png",
"48": "icon.png",
"128": "icon.png"
}
}
popup.html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="popup.css">
</head>
<body>
<div class="container">
<button id="copyBtn" disabled>Copy url</button>
</div>
<script src="popup.js"></script>
</body>
</html>
popup.css
body {
background-color: #e6f7ff;
margin: 5px;
font-family: Arial, sans-serif;
}
#copyBtn {
padding: 5px 10px;
background-color: #1890ff;
border: none;
color: white;
font-size: 1.2em;
transition: background-color 0.5s ease;
text-align: center;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
}
popup.js
let shortenedUrl;
window.onload = function() {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.runtime.sendMessage(
{message: 'fetchUrl', url: tabs[0].url},
function(response) {
shortenedUrl = response.url;
document.getElementById('copyBtn').disabled = false;
}
);
});javaj
};
document.getElementById('copyBtn').addEventListener('click', function() {
navigator.clipboard.writeText(shortenedUrl).then(function() {
console.log('Copying to clipboard was successful!');
const btn = document.getElementById('copyBtn');
btn.innerText = 'Success!';
btn.style.backgroundColor = '#52c41a';
// Close the popup after 2 seconds
setTimeout(window.close, 2000);
}, function(err) {
console.error('Could not copy text: ', err);
});
});
background.js
const backendUrl = 'https://localhost:8080';
// Don't forget to replace this url with the actual backend url after deployment
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.message === 'fetchUrl') {
fetch(backendUrl + 'createUrl', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ url: request.url })
})
.then(response => response.json())
.then(data => {
sendResponse({url: backendUrl + data.key});
})
.catch(err => console.error('Error: ', err));
return true;
}
});
Now lets load the extension in our Chrome browser by following these steps:
- Open the Chrome browser.
- Go to
chrome://extensions/
. - Enable the “Developer mode” toggle on the top right corner.
- Click on the “Load unpacked” button.
- Select the
url-shortener-extension
directory. - The extension will be loaded and available in the Chrome browser.
We’ve successfully built the Chrome extension for our URL shortener.
Now let’s go ahead and dockerize our application.
Step 7: Dockerizing the App
Now we’ll dockerize our application, making it easy to deploy and run in a containerized environment. Let’s get started!
Dockerfile
We create a new file named Dockerfile
in the root directory of our project.
Here we specify the instructions for building the Docker image of our app for deployment.
# Start with a base image containing Java runtime (AdoptOpenJDK)
FROM openjdk:17-jdk-slim AS build
# Set the working directory in the image to "/app"
WORKDIR /app
# Copy the Gradle executable to the image
COPY gradlew ./
# Copy the 'gradle' folder to the image
COPY gradle ./gradle
# Give permission to execute the gradle script
RUN chmod +x ./gradlew
# Copy the rest of the application source code
COPY . .
# Use Gradle to build the application
RUN sh ./gradlew build
# Set up a second stage, which will only keep the compiled application and not the build tools and source code
FROM openjdk:17-jdk-slim
# Set the working directory to '/app'
WORKDIR /app
# Copy the jar file from the first stage
COPY --from=build /app/build/libs/*.jar app.jar
# Set the startup command to execute the jar
CMD ["java", "-jar", "/app/app.jar"]
Docker Compose
Now we will create a docker-compose
in the project’s root directory.
version: '3.8'
services:
url-shortener-backend:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
This docker-compose.yml
file defines the service url-shortener-backend
. The service is based on the configuration in the Dockerfile
. It maps the container's port 8080 to the host's port 8080.
Let’s now navigate to the next section and explore hosting our backend and database with FL0!
Step 8: Hosting our Backend and DB with FL0
Now we would go ahead and host our application with the help of FL0
Setting Up FL0 Database
- In the FL0 dashboard, we would need to create a new project.
- We need to click on “Create a Postgres database”
3. Once the database is created, FL0 will provide us with the necessary database connection details, including the connection URL, username, and password.
4. Add Database Connection Credentials as Environment Variables in fl0-url-shortener-backend
And…Done ✅
Our project is hosted on FL0 successfully.
Conclusion
In this tutorial successfully built a link shortener chrome extension along with it’s backend and database and hosted it using FL0. 🎉
You may find the repository link here https://github.com/dalefl0/fl0-url-shortener-backend.
Moreover, here’s the link for the Chrome Extension if you want to use it yourself https://github.com/dalefl0/URL-Shortener-Chrome-Extension
You may visit https://fl0.com/ to start building and deploying your applications! 🚀