How to create a URL shortener in Spring Boot

By | December 28, 2023

A URL shortener is a service that takes a long URL and generates its short version, which contains a sequence of characters that points to the original URL. The primary purpose of this service is to create more user-friendly and manageable links that are easy to share, especially while sharing over platforms that have limited characters to write, such as Twitter, Instagram captions, SMS, etc. In this tutorial, we are going to create a URL shortener in Spring Boot that will fulfil the following functional requirements:

  1. The service should generate the enough short link to be easily copied and passed to application.
  2. When a end-user access a short link, our application should redirect them to the original link.
  3. Generated short link should have a standard expiration time.

Database Schema

Database Schema
Fig 1- Database Schema

We can generate the short URL with the help of hashing algorithms such as MD-5, SHA-256, Guava, etc.

Steps to create URL shortener in Spring Boot

Step 1- Create a Spring Boot Project

First, we need to create a Spring Boot project. Please refer How to Create a Spring Boot Project? Below is the packaging structure for reference.

URL shortener in Spring Boot
Fig 2- Application packaging structure

Step 2- Add dependencies in Spring Boot

We will be using the following dependencies in our application. We have added a guava maven dependency from Google that will be used for hashing algorithms.

XML
<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>33.0.0-jre</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
			<version>3.14.0</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>io.projectreactor</groupId>
			<artifactId>reactor-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

Step 3- Create a model class

In this step, we will create a model class and map it to our database table.

XML
import java.time.LocalDateTime;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * Model class for url-shortener-service
 * 
 * @author paulsofts
 */

@Document(collection = "tbl_url")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Url {

	private String originalUrl;
	private String shortUrl;
	private LocalDateTime createdAt;
	private LocalDateTime expireAt;

}

Afterward, we will be creating a DTO (Data Transfer Object) class that has fields that need to be passed in the request body while requesting the application endpoints.

Java
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class UrlDto {
	
	private String originalUrl;
	private String expireAt;

}

Step 4- Create Repository interface

Now, we have created a repository interface to persist our data in MongoDB. We have added a custom method getEncodedUrl(String url) to fetch the encoded (short) url from the database.

Java
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.stereotype.Repository;

import com.paulsofts.urlshortenerservice.model.Url;

@Repository
public interface UrlRepository extends MongoRepository<Url, Integer>{
	
	/**
	 * 1. The @Query annotation map the shortUrl to the first
	 *    parameter passed to the method .i.e. url
	 * @param url String
	 * @return Url object
	 */
	@Query("{'shortUrl': ?0}")
	public Url getEncodedUrl(String url);
}

Step 5- Create Service layer

We will develop the service layer of the application. For this, we will create a UrlService interface as follows:

Java
import org.springframework.stereotype.Service;

import com.paulsofts.urlshortenerservice.model.Url;
import com.paulsofts.urlshortenerservice.model.UrlDto;

@Service
public interface UrlService {
	
	public Url generateShortLink(UrlDto urlDto);
	public Url getEncodedUrl(String url);

}

We need to provide the implementation of our interface. For this, we have created a UrlServiceImpl class that implements the UrlService interface and adds definitions to the methods.

Java
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.LocalTime;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.google.common.hash.Hashing;
import com.paulsofts.urlshortenerservice.model.Url;
import com.paulsofts.urlshortenerservice.model.UrlDto;
import com.paulsofts.urlshortenerservice.repository.UrlRepository;

@Component
public class UrlServiceImpl implements UrlService {

	@Autowired
	private UrlRepository urlRepository;

	@Override
	public Url generateShortLink(UrlDto urlDto) {
		Url urlDbObject = new Url();
		if (StringUtils.isNotEmpty(urlDto.getOriginalUrl())) {
			urlDbObject.setOriginalUrl(urlDto.getOriginalUrl());
			String encodedUrl = encodeUrl(urlDto.getOriginalUrl());
			urlDbObject.setShortUrl(encodedUrl);
			urlDbObject.setCreatedAt(LocalDateTime.now().toString());
			urlDbObject.setExpireAt(getExpiryDate(urlDto.getExpireAt(), urlDbObject.getCreatedAt()));
		}
		return this.urlRepository.save(urlDbObject);
	}

	@Override
	public Url getEncodedUrl(String url) {
		return this.urlRepository.getEncodedUrl(url);
	}

	/**
	 * 1. The encodeUrl() method will use guava library 
	 * 	  to generate hashcode for url
	 * 
	 * 2. Adding LocalTime to url, In order to generate unique hashcode everytime
	 * 
	 * @param url
	 * @return String
	 */
	private static String encodeUrl(String url) {
		return Hashing.murmur3_32().hashString(url + LocalTime.now().toString(), StandardCharsets.UTF_8).toString();
	}

	/**
	 * 1. If the end user has given the expiry time we'll take that one
	 *    else, we took the creation time and add 60 seconds to it for 
	 *    the shortUrl to be expired
	 * 
	 * @param expiryDate
	 * @param creationDate
	 * @return
	 */
	private static String getExpiryDate(String expiryDate, String creationDate) {
		if (!StringUtils.isNotEmpty(expiryDate)) {
			return LocalDateTime.parse(creationDate).plusSeconds(60).toString();
		}
		return expiryDate;
	}
}

Step 6- Create Error class

In order to handle exceptions, we are going to create an error class. Which we will throw whenever we receive any empty or null response.

Java
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Error {
	
	private String msg;
	private String status;

}

Step 7- Create a Controller class

Finally, we will create our controller class to manage the request mappings and the responses.

Java
import java.io.IOException;
import java.time.LocalDateTime;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.paulsofts.urlshortenerservice.model.Url;
import com.paulsofts.urlshortenerservice.model.UrlDto;
import com.paulsofts.urlshortenerservice.service.UrlServiceImpl;
import com.paulsofts.urlshortenerservice.util.Error;

import jakarta.servlet.http.HttpServletResponse;

@RestController
@RequestMapping("/api")
public class UrlController {

	@Autowired
	private UrlServiceImpl urlServiceImpl;

	@PostMapping("/url/generate")
	public ResponseEntity<?> generateShortLink(@RequestBody UrlDto urlDto) {
		if (StringUtils.isEmpty(urlDto.getOriginalUrl())) {
			Error error = new Error();
			error.setMsg("Enter a valid url!");
			error.setStatus("400");
			return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
		}
		Url responseUrl = this.urlServiceImpl.generateShortLink(urlDto);
		return new ResponseEntity<Url>(responseUrl, HttpStatus.OK);
	}

	/**
	 * 1. When there is no exception we will be redirecting our shortUrl
	 *    to the original destination page
	 * 
	 * @param shortLink
	 * @param httpServletResponse
	 * @throws IOException
	 */
	@GetMapping("/url/get/{shortLink}")
	public ResponseEntity<?> getShortLink(@PathVariable String shortLink, HttpServletResponse httpServletResponse)
			throws IOException {
		if (StringUtils.isEmpty(shortLink)) {
			Error error = new Error();
			error.setMsg("Url does not exists or it maybe expired");
			error.setStatus("400");
			return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
		}
		Url url = this.urlServiceImpl.getEncodedUrl(shortLink);
		if (LocalDateTime.parse(url.getExpireAt()).isBefore(LocalDateTime.now())) {
			System.out.println("ankur");
			Error error = new Error();
			error.setMsg("Url expired, Please try regenerating it.");
			error.setStatus("400");
			return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
		}
		httpServletResponse.sendRedirect(url.getOriginalUrl());
		return null;
	}

}

Step 8- Test

Please go through the following video to have a complete understanding of the testing scenario.

Leave a Reply

Your email address will not be published. Required fields are marked *