A Spring Boot application that allows users to create shortened URLs, similar to bit.ly or tinyurl. The application includes user authentication, URL management, and click tracking.
- User registration and authentication with JWT
- Create shortened URLs
- Redirect to original URLs via short links
- View user's URL history
- Click tracking for each shortened URL
- Secure endpoints with role-based access
- Spring Boot 3.5.4
- Spring Security 6.5.2
- Spring Data JPA
- MySQL Database
- JWT Authentication
- Lombok
- Maven
shortner/
├── src/
│ ├── main/
│ │ ├── java/com/arpon007/shortner/
│ │ │ ├── ShortnerApplication.java # Main application class
│ │ │ ├── config/
│ │ │ │ └── WebSecurityConfig.java # Security configuration
│ │ │ ├── controller/
│ │ │ │ ├── AuthController.java # Authentication endpoints
│ │ │ │ ├── UrlMappingController.java # URL management endpoints
│ │ │ │ └── RedirectController.java # URL redirect handler
│ │ │ ├── dtos/
│ │ │ │ ├── LoginRequest.java # Login request DTO
│ │ │ │ ├── RegisterRequest.java # Registration request DTO
│ │ │ │ ├── UrlMappingDTO.java # URL mapping response DTO
│ │ │ │ └── ClickEventDtos.java # Click analytics DTO
│ │ │ ├── jwt/
│ │ │ │ ├── JwtAuthenticationResponse.java # JWT response wrapper
│ │ │ │ ├── jwtAuthFilter.java # JWT authentication filter
│ │ │ │ └── JwtUtils.java # JWT utility methods
│ │ │ ├── models/
│ │ │ │ ├── User.java # User entity
│ │ │ │ ├── UrlMapping.java # URL mapping entity
│ │ │ │ └── ClickEvent.java # Click tracking entity
│ │ │ ├── repo/
│ │ │ │ ├── UserRepository.java # User data access
│ │ │ │ ├── UrlMappingRepo.java # URL mapping data access
│ │ │ │ └── ClickEventRepo.java # Click event data access
│ │ │ └── Service/
│ │ │ ├── UserService.java # User business logic
│ │ │ ├── UserDetailsImpl.java # Spring Security user details
│ │ │ ├── UserDetailsServiceImpl.java # User details service
│ │ │ └── UrlMappingService.java # URL management business logic
│ │ └── resources/
│ │ └── application.properties # Application configuration
│ └── test/
│ └── java/com/arpon007/shortner/
│ └── ShortnerApplicationTests.java # Test configuration
├── target/ # Compiled classes (generated)
├── .mvn/wrapper/ # Maven wrapper files
├── ANALYTICS_TESTING_GUIDE.md # Analytics testing documentation
├── Readme.md # This file
├── pom.xml # Maven dependencies
├── mvnw # Maven wrapper (Unix)
└── mvnw.cmd # Maven wrapper (Windows)
- AuthController: Handles user registration and login
- UrlMappingController: Manages URL shortening, analytics, and user URLs
- RedirectController: Handles short URL redirects and click tracking
- User: Stores user account information with encrypted passwords
- UrlMapping: Links short URLs to original URLs with metadata
- ClickEvent: Records individual click events for analytics
- UserService: User management and authentication logic
- UrlMappingService: URL shortening, analytics, and click tracking
- UserDetailsService: Spring Security integration
- WebSecurityConfig: Configures JWT-based authentication
- JwtAuthFilter: Processes JWT tokens in requests
- JwtUtils: Handles JWT token creation and validation
- LoginRequest/RegisterRequest: API request structures
- UrlMappingDTO: URL response format
- ClickEventDtos: Analytics response format
- User registers with username, email, and password
- Password is encrypted using BCrypt
- User logs in to receive JWT token
- JWT token is required for all protected endpoints
- Token expires after 1 hour
- User submits original URL via API
- System generates 8-character random short code
- Mapping is stored in database with user association
- Short URL is returned to user
- User accesses short URL (GET /{shortUrl})
- System looks up original URL
- Click count is incremented
- Click event is recorded with timestamp
- User is redirected to original URL
- Protocol (https://) is added if missing
- Public endpoints:
/api/auth/**(registration, login) - Protected endpoints:
/api/url/**(requires JWT) - Open redirects:
/{shortUrl}(public access for redirects) - User isolation: Users can only access their own URLs
- Java 21 or higher
- MySQL 8.0+ Database
- Maven 3.6+
- IDE (IntelliJ IDEA, Eclipse, VS Code) - Optional
- Postman or similar API testing tool
Ensure Java 21 is installed and JAVA_HOME is set:
java -version
# Should show Java 21.x.x-- Create database
CREATE DATABASE url_shortner;
-- Create user (optional, for security)
CREATE USER 'shortner_user'@'localhost' IDENTIFIED BY 'your_secure_password';
GRANT ALL PRIVILEGES ON url_shortner.* TO 'shortner_user'@'localhost';
FLUSH PRIVILEGES;Update src/main/resources/application.properties:
# Application Name
spring.application.name=shortner
# Database Configuration
spring.datasource.url=jdbc:mysql://localhost:3306/url_shortner
spring.datasource.username=root
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA/Hibernate Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
# JWT Configuration
jwt.secret=your_jwt_secret_key_minimum_32_characters_long
jwt.expiration=3600000Important Security Notes:
- Change
jwt.secretto a secure random string in production - Use environment variables for sensitive data in production
- Consider using
spring.jpa.hibernate.ddl-auto=validatein production
The application automatically creates the following tables using JPA/Hibernate:
CREATE TABLE users (
id BIGINT NOT NULL AUTO_INCREMENT,
email VARCHAR(255),
username VARCHAR(255),
password VARCHAR(255),
role VARCHAR(255) DEFAULT 'ROLE_USER',
PRIMARY KEY (id),
UNIQUE KEY unique_username (username),
UNIQUE KEY unique_email (email)
);CREATE TABLE url_mapping (
id BIGINT NOT NULL AUTO_INCREMENT,
original_url VARCHAR(2048),
short_url VARCHAR(255),
click_count INT DEFAULT 0,
created_date DATETIME(6),
user_id BIGINT,
PRIMARY KEY (id),
UNIQUE KEY unique_short_url (short_url),
KEY fk_user (user_id),
CONSTRAINT fk_url_mapping_user FOREIGN KEY (user_id) REFERENCES users (id)
);CREATE TABLE click_event (
id BIGINT NOT NULL AUTO_INCREMENT,
click_date DATETIME(6),
user_mapping_id BIGINT,
PRIMARY KEY (id),
KEY fk_url_mapping (user_mapping_id),
CONSTRAINT fk_click_event_mapping FOREIGN KEY (user_mapping_id) REFERENCES url_mapping (id)
);- One-to-Many: User → UrlMapping (One user can have multiple URLs)
- One-to-Many: UrlMapping → ClickEvent (One URL can have multiple clicks)
- Many-to-One: ClickEvent → UrlMapping (Many clicks belong to one URL)
# Clone the repository
git clone https://github.com/arpondark/shortner-spring-boot.git
cd shortner-spring-boot
# Make Maven wrapper executable (Unix/Linux/Mac)
chmod +x mvnw
# Run the application
./mvnw spring-boot:run
# Windows
mvnw.cmd spring-boot:run# Clean and compile
mvn clean compile
# Run tests
mvn test
# Package the application
mvn package
# Run the packaged JAR
java -jar target/shortner-0.0.1-SNAPSHOT.jar
# Or run directly
mvn spring-boot:run- Import the project as a Maven project
- Ensure Java 21 is configured
- Run
ShortnerApplication.javamain method - Application will start on
http://localhost:8080
# Check if application is running
curl http://localhost:8080/actuator/health
# Or test a public endpoint
curl -X POST http://localhost:8080/api/auth/public/register \
-H "Content-Type: application/json" \
-d '{"username":"test","email":"test@example.com","password":"test123"}'<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency><dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency><dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency><dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency><properties>
<java.version>21</java.version>
</properties><plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>@Data- Generates getters, setters, toString, equals, hashCode@AllArgsConstructor- Generates constructor with all fields@NoArgsConstructor- Generates no-argument constructor@Entity- JPA entity marker@RestController- Spring REST controller marker@Service- Spring service component marker@Repository- Spring repository component marker
- URL:
POST /api/auth/register - Content-Type:
application/json - Body:
{
"username": "john_doe",
"email": "john@example.com",
"password": "password123"
}- Response:
{
"message": "User registered successfully"
}- URL:
POST /api/auth/login - Content-Type:
application/json - Body:
{
"username": "john_doe",
"password": "password123"
}- Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer"
}- URL:
POST /api/url/shorten - Content-Type:
application/json - Headers:
Authorization: Bearer {jwt_token} - Body:
{
"originalUrl": "https://www.example.com/very/long/url/that/needs/shortening"
}- Response:
{
"id": 1,
"orginalurl": "https://www.example.com/very/long/url/that/needs/shortening",
"shorturl": "AbC12xYz",
"clickCount": 0,
"createdAt": "2025-08-04T14:30:00",
"username": "john_doe"
}- URL:
GET /api/url/myurls - Headers:
Authorization: Bearer {jwt_token} - Response:
[
{
"id": 1,
"orginalurl": "https://www.example.com/very/long/url/that/needs/shortening",
"shorturl": "AbC12xYz",
"clickCount": 5,
"createdAt": "2025-08-04T14:30:00",
"username": "john_doe"
}
]- URL:
GET /api/url/analytics/{shortUrl} - Headers:
Authorization: Bearer {jwt_token} - Query Parameters:
startDate(optional): Start date in YYYY-MM-DD formatendDate(optional): End date in YYYY-MM-DD format
- Example:
GET /api/url/analytics/AbC12xYz?startDate=2025-08-01&endDate=2025-08-04 - Response:
{
"shortUrl": "AbC12xYz",
"originalUrl": "https://www.example.com/very/long/url/that/needs/shortening",
"totalClicks": 15,
"uniqueClicks": 12,
"clicksByDate": [
{
"date": "2025-08-01",
"clicks": 3
},
{
"date": "2025-08-02",
"clicks": 7
},
{
"date": "2025-08-03",
"clicks": 4
},
{
"date": "2025-08-04",
"clicks": 1
}
],
"topReferrers": [
{
"referrer": "direct",
"count": 8
},
{
"referrer": "google.com",
"count": 4
},
{
"referrer": "facebook.com",
"count": 3
}
],
"deviceTypes": [
{
"device": "desktop",
"count": 9
},
{
"device": "mobile",
"count": 5
},
{
"device": "tablet",
"count": 1
}
]
}- URL:
GET /{shortUrl} - Example:
GET /AbC12xYz - Response: HTTP 302 redirect to the original URL
- Note: This endpoint increments the click count automatically
- Create a new POST request to
http://localhost:8080/api/auth/register - Set Content-Type to
application/json - Add the registration JSON in the body
- Send the request
- Create a new POST request to
http://localhost:8080/api/auth/login - Set Content-Type to
application/json - Add the login JSON in the body
- Send the request
- Copy the
tokenvalue from the response
- Create a new POST request to
http://localhost:8080/api/url/shorten - Set Content-Type to
application/json - Add Authorization header:
Bearer {paste_your_token_here} - Add the URL shortening JSON in the body
- Send the request
- Copy the
shorturlvalue from the response
- Create a new GET request to
http://localhost:8080/api/url/myurls - Add Authorization header:
Bearer {paste_your_token_here} - Send the request
- Open a browser or create a GET request in Postman
- Navigate to
http://localhost:8080/{shorturl}(replace {shorturl} with the actual short URL) - You should be redirected to the original URL
- Create a new GET request to
http://localhost:8080/api/url/analytics/{shorturl} - Add Authorization header:
Bearer {paste_your_token_here} - Optionally add query parameters for date filtering:
?startDate=2025-08-01&endDate=2025-08-04
- Send the request
Example cURL command:
curl -X GET "http://localhost:8080/api/url/analytics/AbC12xYz?startDate=2025-08-01&endDate=2025-08-04" \
-H "Authorization: Bearer {your_jwt_token_here}"{
"username": "testuser",
"email": "test@example.com",
"password": "test123"
}{
"originalUrl": "https://www.google.com"
}{
"originalUrl": "https://github.com/spring-projects/spring-boot"
}{
"originalUrl": "https://stackoverflow.com/questions/tagged/spring-boot"
}Get analytics for a specific short URL:
GET /api/url/analytics/AbC12xYzGet analytics with date filtering:
GET /api/url/analytics/AbC12xYz?startDate=2025-08-01&endDate=2025-08-04Get analytics for the last 7 days:
GET /api/url/analytics/AbC12xYz?startDate=2025-07-28&endDate=2025-08-04com.arpon007.shortner- Root packageconfig- Configuration classes (Security, etc.)controller- REST API controllersdtos- Data Transfer Objectsjwt- JWT-related utilities and filtersmodels- JPA entitiesrepo- Spring Data repositoriesService- Business logic services
- Repository Pattern: Data access abstraction via Spring Data JPA
- DTO Pattern: Separate request/response objects from entities
- Service Layer: Business logic separation from controllers
- Filter Pattern: JWT authentication via custom filter
- Builder Pattern: Used implicitly via Lombok annotations
@Entity
@Data
public class NewEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Add fields and relationships
}@Repository
public interface NewEntityRepository extends JpaRepository<NewEntity, Long> {
// Add custom query methods
}@Service
@AllArgsConstructor
public class NewEntityService {
private NewEntityRepository repository;
// Add business logic methods
}@RestController
@RequestMapping("/api/newentity")
@AllArgsConstructor
public class NewEntityController {
private NewEntityService service;
@GetMapping
@PreAuthorize("hasRole('USER')")
public ResponseEntity<List<NewEntity>> getAll(Principal principal) {
// Implementation
}
}- User credentials validated against database
- JWT token generated with user details and role
- Token sent to client in response
- Client includes token in Authorization header
- JwtAuthFilter validates token on each request
- SecurityContext populated with user details
// In WebSecurityConfig.java
.requestMatchers("/api/public/**").permitAll() // Public access
.requestMatchers("/api/admin/**").hasRole("ADMIN") // Admin only
.requestMatchers("/api/user/**").hasRole("USER") // User only
.anyRequest().authenticated() // Default: authenticated# Run all tests
mvn test
# Run specific test class
mvn test -Dtest=UrlMappingServiceTest
# Run with coverage
mvn test jacoco:report# Test with embedded database
mvn test -Dspring.profiles.active=test
# Test specific endpoints
curl -X POST http://localhost:8080/api/auth/public/register \
-H "Content-Type: application/json" \
-d '{"username":"testuser","email":"test@example.com","password":"test123"}'- Indexing: Add indexes on frequently queried columns
CREATE INDEX idx_short_url ON url_mapping(short_url);
CREATE INDEX idx_user_id ON url_mapping(user_id);
CREATE INDEX idx_click_date ON click_event(click_date);- Query Optimization: Use JPQL for complex queries
@Query("SELECT NEW com.arpon007.shortner.dtos.ClickEventDtos(DATE(c.clickDate), COUNT(c)) " +
"FROM ClickEvent c WHERE c.urlMapping = :urlMapping " +
"GROUP BY DATE(c.clickDate)")
List<ClickEventDtos> findClickStatsByUrlMapping(@Param("urlMapping") UrlMapping urlMapping);// Add caching to frequently accessed data
@Cacheable("shortUrls")
public UrlMapping findByShortUrl(String shortUrl) {
return urlMappingRepo.findByShortUrl(shortUrl).orElse(null);
}# application-prod.properties
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
logging.level.com.arpon007.shortner=WARN
# Use environment variables
spring.datasource.url=${DB_URL:jdbc:mysql://localhost:3306/url_shortner}
spring.datasource.username=${DB_USERNAME:root}
spring.datasource.password=${DB_PASSWORD}
jwt.secret=${JWT_SECRET}FROM openjdk:21-jre-slim
COPY target/shortner-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- DB_URL=jdbc:mysql://db:3306/url_shortner
- DB_USERNAME=root
- DB_PASSWORD=password
depends_on:
- db
db:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=password
- MYSQL_DATABASE=url_shortner
ports:
- "3306:3306"# Create JAR file
mvn clean package
# Build Docker image
docker build -t url-shortener .
# Run with Docker Compose
docker-compose up -d// Add logging to classes
private static final Logger logger = LoggerFactory.getLogger(UrlMappingService.class);
public UrlMappingDTO createShortUrl(String originalUrl, User user) {
logger.info("Creating short URL for user: {}, original URL: {}", user.getUsername(), originalUrl);
// Implementation
}# Enable actuator endpoints
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=alwaysThe application returns appropriate HTTP status codes:
200 OK- Successful requests201 Created- Successful user registration400 Bad Request- Invalid request data401 Unauthorized- Missing or invalid JWT token403 Forbidden- Insufficient permissions404 Not Found- Resource not found (e.g., invalid short URL)500 Internal Server Error- Server errors
Error: "path": "/api/url//myurls"
Solution: Ensure you're using the correct URL: http://localhost:8080/api/url/myurls (single slash)
Error: 401 Unauthorized
Solution: Login again to get a new JWT token
Error: Database connection failed Solution: Verify MySQL is running and database credentials are correct
The application automatically creates the following tables:
users- User information and credentialsurl_mapping- URL mappings with click trackingclick_event- Individual click events (for detailed analytics)
- Passwords are encrypted using BCrypt
- JWT tokens expire after 1 hour
- All URL management endpoints require authentication
- Role-based access control with USER role
- The application uses Lombok to reduce boilerplate code
- All entities use
@Dataannotation for automatic getter/setter generation - JWT secret should be changed in production environments
- Database credentials should be externalized in production
- Follow Java naming conventions (camelCase for variables, PascalCase for classes)
- Use meaningful variable and method names
- Add proper JavaDoc comments for public methods
- Keep methods small and focused on single responsibility
- Create a feature branch from
main - Make changes and commit with descriptive messages
- Test your changes thoroughly
- Submit a pull request with detailed description
- Write unit tests for new features
- Ensure all existing tests pass
- Test endpoints with both valid and invalid data
- Verify JWT authentication works correctly
- Update the appropriate service/controller classes
- Add new DTOs if needed for request/response
- Update database schema if required
- Add appropriate security configuration
- Update this README with new endpoints
# Check if port 8080 is already in use
netstat -ano | findstr :8080
# Kill process using the port
taskkill /PID <process_id> /F# Verify MySQL service is running
services.msc
# Test connection
mysql -u root -p -h localhost -P 3306- Ensure token is included in Authorization header
- Check token hasn't expired (1 hour lifetime)
- Verify token format:
Bearer <token>
- Check if original URL includes protocol (http/https)
- Verify short URL exists in database
- Confirm click events are being recorded
# Enable debug logging
logging.level.com.arpon007.shortner=DEBUG
logging.level.org.springframework.security=DEBUG- Custom short URL aliases
- URL expiration dates
- Detailed analytics dashboard
- QR code generation for short URLs
- Rate limiting for URL creation
- Email verification for user registration
- Password reset functionality
- Admin panel for user management
- Redis caching for frequently accessed URLs
- API rate limiting with Redis
- Database connection pooling optimization
- Comprehensive integration tests
- API documentation with Swagger
- Containerized development environment
- CI/CD pipeline setup
This project is open source and available under the MIT License.
For questions or support, please contact the development team or create an issue in the project repository.
Project Status: Active Development
Version: 1.0.0
Last Updated: December 2024