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:
- The service should generate the enough short link to be easily copied and passed to application.
- When a end-user access a short link, our application should redirect them to the original link.
- Generated short link should have a standard expiration time.
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.
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.
<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.
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.
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.
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:
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.
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.
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.
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.