Contenidos
Creando Tiniest
Un acortador de URL moderno con Spring Boot.
Tiniest es un servicio rápido y minimalista para acortar URLs construido con Spring Boot 4.0 y Java 24, escrito y deployado en menos de un día. No es un proyecto especialmente técnico, pero quería comprobar qué tan bueno (o molesto) sería crear un proyecto pequeño que normalmente se crearía con Javascript o Python, pero usando probablemente el mejor ecosistema empresarial que existe.
Cómo funciona
Primero, hablemos un poco sobre cómo funciona el algoritmo.
Paso 1: Hash SHA-256
La URL de entrada se procesa con SHA-256, una función de hash criptográfica que genera un resumen de 256 bits (32 bytes). Esto garantiza:
- Determinismo: la misma URL siempre produce el mismo hash
- Distribución uniforme: incluso URLs similares producen hashes muy distintos
- Resistencia de colisiones: es extremadamente difícil encontrar dos entradas diferentes con el mismo hash
Paso 2: Cortar y codificar con Base62
Tomar el hash completo de 32 bytes sería demasiado; en lugar de eso solo tomo una porción de los primeros 6 bytes del resultado.
Esos 6 bytes se codifican usando Base62 porque es seguro para URLs, legible por humanos y más compacto que la codificación hexadecimal.
Finalmente obtenemos códigos cortos como “7kPv2Nm” o “xR3qLp9”, típicamente de 8 a 10 caracteres.
Spring Boot: cuando el framework hace el todo el trabajo (este título funciona mejor en inglés)
Java es famoso por ser un lenguaje con mucho boilerplate o código repetitivo. En mi opinión eso es en su mayoría una crítica a Java < 8. Java moderno ofrece muchas herramientas para evitar código verbose si es necesario; también hay gente que sigue prefiriendo escribir código más verbose por razones de claridad.
// Esto funciona
ArrayList<Integer> numbers = new ArrayList<Integer>();
// Esto también funciona
var numbers = List.of(1, 2);
Además de eso, tenemos Spring Boot.
Spring Boot nos permite crear aplicaciones web empresariales con mínimo código repetitivo.
Magia de la auto-configuración
¿El arranque de la aplicación completa? Solo esto:
@SpringBootApplication
public class TiniestApplication {
public static void main(String[] args) {
SpringApplication.run(TiniestApplication.class, args);
}
}
Esa única anotación ‘@SpringBootApplication’ habilita:
- Escaneo de componentes
- Auto-configuración para JPA, servidor web, validación y más
- Configuración de fuentes de propiedades
Acceso a datos sin boilerplate
La capa de repositorio es donde Spring Data JPA brilla:
public interface UrlRepository extends JpaRepository<Url, Long> {
Optional<Url> findByMinifiedPath(String minifiedPath);
Optional<Url> findByPath(String path);
@Modifying
@Query("UPDATE Url u SET u.clickCount = u.clickCount + 1 WHERE u.minifiedPath = :minifiedPath")
void incrementClickCount(String minifiedPath);
}
¡Eso es todo! Spring genera:
- Todas las operaciones CRUD automáticamente
- Métodos de consulta derivados del nombre del método (‘findByMinifiedPath’, ‘findByPath’)
- Consultas de actualización personalizadas con ‘@Query’
Controladores REST en minutos
Construir APIs REST es casi trivial:
@RestController
@RequestMapping("/api/urls")
public class ApiController {
@PostMapping
public ResponseEntity<ShortenResponse> shorten(
@Valid @RequestBody ShortenRequest request,
HttpServletRequest httpRequest) {
Url url = urlService.shortenUrl(request.url());
return ResponseEntity.status(HttpStatus.CREATED).body(toResponse(url, httpRequest));
}
@GetMapping("/{shortCode}")
public ResponseEntity<ShortenResponse> getStats(@PathVariable String shortCode) {
Url url = urlService.getUrlStats(shortCode);
return ResponseEntity.ok(toResponse(url));
}
}
Normalmente creas controladores por cada grupo de endpoints, por ejemplo:
- /users
- /urls
- /posts
Pero como este fue un proyecto muy pequeño, creé un único controlador que contiene los dos endpoints que necesitamos.
Spring se encarga de:
- Serialización/deserialización JSON
- Validación de solicitudes con Bean Validation
- Extracción de variables de ruta
- Mapeo de códigos de estado HTTP
Viniendo de Node.js/JavaScript, la mayoría de esto puede no ser tan impresionante, pero los lenguajes tipados (especialmente los que no usan reflexión, mirándote a ti, Rust) pueden complicar el trabajo con datos JSON.
Records de Java para DTOs
¿Recuerdas cuando dije que Java no es tan verbose como la gente cree? Pues tenemos Records:
Los records son básicamente clases diseñadas para transportar datos inmutables; reducen mucho el boilerplate.
public record ShortenRequest(
@NotBlank(message = "URL cannot be blank")
@URL(message = "Must be a valid URL")
String url
) {}
public record ShortenResponse(
Long id,
String originalUrl,
String shortCode,
String shortUrl,
Long clickCount,
Instant createdAt
) {}
Los records proporcionan automáticamente constructores, getters, ‘equals()’, ‘hashCode()’ y ‘toString()’. Combinados con anotaciones de validación, son perfectos para DTOs.
Lamentablemente, los records en Java no evitan la asignación de memoria en el heap como en C#, pero eso es otro tema.
Manejo global de excepciones
Una clase se encarga de todas las respuestas de error de forma consistente:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UrlNotFoundException.class)
public ResponseEntity<Map<String, Object>> handleUrlNotFound(UrlNotFoundException ex) {
// Return structured error response
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationErrors(...) {
// Return validation errors
}
}
Migraciones de base de datos con Flyway
La integración con Flyway es automática. Simplemente, coloca los archivos SQL en la carpeta correspondiente y listo:
-- V1__initial_migration.sql
CREATE TABLE url
(
id BIGSERIAL PRIMARY KEY,
path TEXT NOT NULL,
minified_path TEXT NOT NULL UNIQUE
);
-- V2__add_tracking_columns.sql
ALTER TABLE url
ADD COLUMN click_count BIGINT NOT NULL DEFAULT 0;
ALTER TABLE url
ADD COLUMN created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW();
CREATE INDEX idx_url_minified_path ON url (minified_path);
Spring Boot detecta Flyway en el classpath y ejecuta las migraciones automáticamente al iniciar. Control de versiones para el esquema de la base de datos, con cero configuración.
Despliegue: Docker para todo
Decidí desplegar con Docker para que, con mi instancia de Coolify en funcionamiento, pueda hacer despliegues automáticos al hacer commit.
Conclusión
Para mí, construir Tiniest demuestra el poder del desarrollo moderno en Java:
- Spring Boot y Java moderno eliminan el boilerplate y te permiten concentrarte en la lógica del código.
- Docker hace que el despliegue sea consistente entre entornos.
- Los perfiles de Compose adaptan la misma base de código desde desarrollo hasta producción.
El código completo es muy pequeño: alrededor de 12 archivos Java, y aun así está listo para producción.
Prueba tiniest en producción!