Building a User-Friendly URL Shortener Using Spring Boot, Postgres, and FL0

Dale Brett
FL0 Engineering
14 min readJul 27, 2023

--

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:

  1. Navigation: The user opens Chrome and visits the webpage he/she wants to shorten using our installed extension.
  2. 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.
  3. 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 🌱

  1. Let’s visit start.spring.io. This is the Spring Initializr, a web-based tool that helps in creating Spring Boot projects quickly.
  2. 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👇

Spring Initializr

Setting Up the Project

  1. We will extract the downloaded zip file to a preferred location.
  2. 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.
  3. 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.
  4. 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

  1. Open the application.yml file located in the src/main/resources directory. This file allows us to define properties for our application.
  2. 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 and spring.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

  1. Open the application.yml file located in the src/main/resources directory. This file allows us to define properties for our application.
  2. 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 and spring.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

  1. Create a new Java class named ShortUrlEntity in a new package com.fl0.urlshortener.entity
  2. 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;
}
  1. 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.
  2. 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

  1. Create a new Java interface named ShortUrlRepository in a new package com.fl0.urlshortener.repository
  2. 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);
}
  1. The ShortUrlRepository extends the JpaRepository interface provided by Spring Data JPA. It enables us to perform CRUD operations on the ShortUrlEntity easily.
  2. We will also define two custom methods:
  • findByKey: Retrieves a ShortUrlEntity entity based on the given key.
  • findByFullUrl: Retrieves a ShortUrlEntity 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)

  1. Let’s create a new Java class named ShortUrlRequest in the com.example.urlshortener.dto package.
  2. 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;
}
  1. The ShortUrlRequest DTO represents a request to create a shortened URL. It has a single field, url.
  2. We will create another Java class named ShortUrlResponse in the same package.
  3. 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;
}
  1. 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

  1. Create a new Java class named UrlShortenerService in the com.fl0.urlshortener.service package.
  2. 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.

  1. We create a new Java class named CorsConfig in the com.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");
}
}
  1. The CorsConfig class implements the WebMvcConfigurer interface, allowing us to customize the CORS configuration for our application.
  2. With this configuration, our backend will allow requests from the Chrome extension, enabling communication between the extension and the API.
  3. 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!

  1. 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:

  1. Open the Chrome browser.
  2. Go to chrome://extensions/.
  3. Enable the “Developer mode” toggle on the top right corner.
  4. Click on the “Load unpacked” button.
  5. Select the url-shortener-extension directory.
  6. 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

  1. In the FL0 dashboard, we would need to create a new project.
  2. 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! 🚀

--

--

Dale Brett
FL0 Engineering

Founder & CEO at FL0 | Backend engineering, supercharged