diff --git a/pom.xml b/pom.xml
index 6c34601c..8b415da1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,8 +2,8 @@
4.0.0
com.tpximpact
- url_shortener_api
- 1.0
+ url_shortener
+ 1.0.0
org.springframework.boot
@@ -16,17 +16,42 @@
-
+
org.springframework.boot
- spring-boot-starter
+ spring-boot-starter-web
-
+
org.springframework.boot
- spring-boot-starter-web
+ spring-boot-starter-data-jpa
-
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
diff --git a/src/main/java/com/tpximpact/urlshortener/UrlShortenerApplication.java b/src/main/java/com/tpximpact/urlshortener/UrlShortenerApplication.java
new file mode 100644
index 00000000..cdf3eb69
--- /dev/null
+++ b/src/main/java/com/tpximpact/urlshortener/UrlShortenerApplication.java
@@ -0,0 +1,13 @@
+package com.tpximpact.urlshortener;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+class UrlShortenerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(UrlShortenerApplication.class, args);
+ }
+
+}
diff --git a/src/main/java/com/tpximpact/urlshortener/controller/UrlController.java b/src/main/java/com/tpximpact/urlshortener/controller/UrlController.java
new file mode 100644
index 00000000..a46cb266
--- /dev/null
+++ b/src/main/java/com/tpximpact/urlshortener/controller/UrlController.java
@@ -0,0 +1,53 @@
+package com.tpximpact.urlshortener.controller;
+
+import com.tpximpact.urlshortener.dto.UrlListResponse;
+import com.tpximpact.urlshortener.dto.UrlRequest;
+import com.tpximpact.urlshortener.dto.UrlResponse;
+import com.tpximpact.urlshortener.service.UrlService;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+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.RestController;
+import java.net.URI;
+import java.util.List;
+
+@RestController
+@RequiredArgsConstructor
+@Slf4j
+class UrlController {
+ private final UrlService urlService;
+
+ @PostMapping("/shorten")
+ public ResponseEntity shorten(@RequestBody @Valid UrlRequest request) {
+ UrlResponse response = urlService.shorten(request);
+ return ResponseEntity.status(201).body(response);
+ }
+
+ @GetMapping("/{alias}")
+ public ResponseEntity redirect(@PathVariable String alias) {
+ log.info("Contacting urlService to full url with: {}", alias);
+ String fullUrl = urlService.getFullUrl(alias);
+ log.info("Received Full url to redirect: {}", fullUrl);
+ return ResponseEntity.status(302)
+ .location(URI.create(fullUrl))
+ .build();
+ }
+
+ @GetMapping("/urls")
+ public ResponseEntity> getAll() {
+ return ResponseEntity.ok(urlService.getAll());
+ }
+
+ @DeleteMapping("/{alias}")
+ public ResponseEntity delete(@PathVariable String alias) {
+ urlService.delete(alias);
+ return ResponseEntity.noContent().build();
+ }
+
+}
diff --git a/src/main/java/com/tpximpact/urlshortener/dto/UrlListResponse.java b/src/main/java/com/tpximpact/urlshortener/dto/UrlListResponse.java
new file mode 100644
index 00000000..f61f25a6
--- /dev/null
+++ b/src/main/java/com/tpximpact/urlshortener/dto/UrlListResponse.java
@@ -0,0 +1,7 @@
+package com.tpximpact.urlshortener.dto;
+
+public record UrlListResponse(
+ String alias,
+ String fullUrl,
+ String shortUrl
+) {}
\ No newline at end of file
diff --git a/src/main/java/com/tpximpact/urlshortener/dto/UrlRequest.java b/src/main/java/com/tpximpact/urlshortener/dto/UrlRequest.java
new file mode 100644
index 00000000..79bb75b4
--- /dev/null
+++ b/src/main/java/com/tpximpact/urlshortener/dto/UrlRequest.java
@@ -0,0 +1,8 @@
+package com.tpximpact.urlshortener.dto;
+
+import jakarta.validation.constraints.NotBlank;
+
+public record UrlRequest (
+ @NotBlank String fullUrl,
+ String customAlias
+) {}
diff --git a/src/main/java/com/tpximpact/urlshortener/dto/UrlResponse.java b/src/main/java/com/tpximpact/urlshortener/dto/UrlResponse.java
new file mode 100644
index 00000000..9c9d7d9e
--- /dev/null
+++ b/src/main/java/com/tpximpact/urlshortener/dto/UrlResponse.java
@@ -0,0 +1,5 @@
+package com.tpximpact.urlshortener.dto;
+
+public record UrlResponse(
+ String shortUrl
+) {}
diff --git a/src/main/java/com/tpximpact/urlshortener/entity/Url.java b/src/main/java/com/tpximpact/urlshortener/entity/Url.java
new file mode 100644
index 00000000..8dff4267
--- /dev/null
+++ b/src/main/java/com/tpximpact/urlshortener/entity/Url.java
@@ -0,0 +1,35 @@
+package com.tpximpact.urlshortener.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(name = "urls", uniqueConstraints = {
+ @UniqueConstraint(columnNames = "alias")
+})
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class Url {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false)
+ private String fullUrl;
+
+ @Column(nullable = false, unique = true)
+ private String alias;
+}
diff --git a/src/main/java/com/tpximpact/urlshortener/exception/BadRequestException.java b/src/main/java/com/tpximpact/urlshortener/exception/BadRequestException.java
new file mode 100644
index 00000000..0490e90c
--- /dev/null
+++ b/src/main/java/com/tpximpact/urlshortener/exception/BadRequestException.java
@@ -0,0 +1,5 @@
+package com.tpximpact.urlshortener.exception;
+
+public class BadRequestException extends RuntimeException {
+ public BadRequestException(String message) { super(message); }
+}
diff --git a/src/main/java/com/tpximpact/urlshortener/exception/GlobalExceptionHandler.java b/src/main/java/com/tpximpact/urlshortener/exception/GlobalExceptionHandler.java
new file mode 100644
index 00000000..01c41b00
--- /dev/null
+++ b/src/main/java/com/tpximpact/urlshortener/exception/GlobalExceptionHandler.java
@@ -0,0 +1,19 @@
+package com.tpximpact.urlshortener.exception;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(BadRequestException.class)
+ public ResponseEntity handleBadRequest(BadRequestException ex) {
+ return ResponseEntity.badRequest().body(ex.getMessage());
+ }
+
+ @ExceptionHandler(NotFoundException.class)
+ public ResponseEntity handleNotFound(NotFoundException ex) {
+ return ResponseEntity.status(404).body(ex.getMessage());
+ }
+}
diff --git a/src/main/java/com/tpximpact/urlshortener/exception/NotFoundException.java b/src/main/java/com/tpximpact/urlshortener/exception/NotFoundException.java
new file mode 100644
index 00000000..5042cfb3
--- /dev/null
+++ b/src/main/java/com/tpximpact/urlshortener/exception/NotFoundException.java
@@ -0,0 +1,7 @@
+package com.tpximpact.urlshortener.exception;
+
+public class NotFoundException extends RuntimeException {
+ public NotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/tpximpact/urlshortener/repository/UrlRepository.java b/src/main/java/com/tpximpact/urlshortener/repository/UrlRepository.java
new file mode 100644
index 00000000..877be95e
--- /dev/null
+++ b/src/main/java/com/tpximpact/urlshortener/repository/UrlRepository.java
@@ -0,0 +1,15 @@
+package com.tpximpact.urlshortener.repository;
+
+import com.tpximpact.urlshortener.entity.Url;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface UrlRepository extends JpaRepository {
+
+ boolean existsByAlias(String alias);
+
+ Optional findByAlias(String alias);
+}
diff --git a/src/main/java/com/tpximpact/urlshortener/service/UrlService.java b/src/main/java/com/tpximpact/urlshortener/service/UrlService.java
new file mode 100644
index 00000000..7b948ed4
--- /dev/null
+++ b/src/main/java/com/tpximpact/urlshortener/service/UrlService.java
@@ -0,0 +1,18 @@
+package com.tpximpact.urlshortener.service;
+
+import com.tpximpact.urlshortener.dto.UrlListResponse;
+import com.tpximpact.urlshortener.dto.UrlRequest;
+import com.tpximpact.urlshortener.dto.UrlResponse;
+
+import java.util.List;
+
+public interface UrlService {
+
+ UrlResponse shorten(UrlRequest request);
+
+ String getFullUrl(String alias);
+
+ List getAll();
+
+ void delete(String alias);
+}
diff --git a/src/main/java/com/tpximpact/urlshortener/service/UrlServiceImpl.java b/src/main/java/com/tpximpact/urlshortener/service/UrlServiceImpl.java
new file mode 100644
index 00000000..c9efd817
--- /dev/null
+++ b/src/main/java/com/tpximpact/urlshortener/service/UrlServiceImpl.java
@@ -0,0 +1,71 @@
+package com.tpximpact.urlshortener.service;
+
+import com.tpximpact.urlshortener.dto.UrlListResponse;
+import com.tpximpact.urlshortener.dto.UrlRequest;
+import com.tpximpact.urlshortener.dto.UrlResponse;
+import com.tpximpact.urlshortener.entity.Url;
+import com.tpximpact.urlshortener.exception.BadRequestException;
+import com.tpximpact.urlshortener.exception.NotFoundException;
+import com.tpximpact.urlshortener.repository.UrlRepository;
+import com.tpximpact.urlshortener.util.UrlUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class UrlServiceImpl implements UrlService {
+ private static final String BASE_URL = "http://localhost:8080/";
+ private final UrlRepository urlRepository;
+
+ @Override
+ public UrlResponse shorten(UrlRequest request) {
+
+ String alias = request.customAlias() != null
+ ? request.customAlias() : UrlUtils.generateAlias();
+
+ if (urlRepository.existsByAlias(alias)) {
+ throw new BadRequestException("Invalid input or alias already taken");
+ }
+
+ Url url = Url
+ .builder()
+ .fullUrl(request.fullUrl())
+ .alias(alias)
+ .build();
+
+ urlRepository.save(url);
+
+ return new UrlResponse(BASE_URL + alias);
+ }
+
+ @Override
+ public String getFullUrl(String alias) {
+ log.info("Getting full url for alias ' {} ' from the DB", alias);
+ return urlRepository.findByAlias(alias)
+ .map(Url::getFullUrl)
+ .orElseThrow(() -> new NotFoundException("Alias not found"));
+ }
+
+ @Override
+ public List getAll() {
+ return urlRepository.findAll().stream()
+ .map(url -> new UrlListResponse(
+ url.getAlias(),
+ url.getFullUrl(),
+ BASE_URL + url.getAlias()
+ ))
+ .toList();
+ }
+
+ public void delete(String alias) {
+
+ Url url = urlRepository.findByAlias(alias)
+ .orElseThrow(() -> new NotFoundException("Alias not found"));
+ log.info("Deleting shorten url for alias '{}' from the DB", url.getAlias());
+ urlRepository.delete(url);
+ }
+}
diff --git a/src/main/java/com/tpximpact/urlshortener/util/UrlUtils.java b/src/main/java/com/tpximpact/urlshortener/util/UrlUtils.java
new file mode 100644
index 00000000..46e50cfe
--- /dev/null
+++ b/src/main/java/com/tpximpact/urlshortener/util/UrlUtils.java
@@ -0,0 +1,13 @@
+package com.tpximpact.urlshortener.util;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.UUID;
+
+@Slf4j
+public class UrlUtils {
+
+ public static String generateAlias() {
+ return UUID.randomUUID().toString().substring(0, 6);
+ }
+}
diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml
new file mode 100644
index 00000000..819ffce0
--- /dev/null
+++ b/src/main/resources/application-dev.yaml
@@ -0,0 +1,16 @@
+spring:
+ datasource:
+ url: jdbc:h2:file:./data/mydb
+ driver-class-name: org.h2.Driver
+ username: sa
+ password:
+
+ h2:
+ console:
+ enabled: true
+ path: /h2-console
+
+ jpa:
+ show_sql: true
+ hibernate:
+ ddl-auto: update