📡
CampañasPro API
Servidor Ktor — SaaS B2B de gestión de campañas de marketing
Kotlin 1.9 Ktor 2.3 Koin 3.5 JWT Auth testApplication
Resumen del proyecto
8
Endpoints REST
6
Plugins Ktor
4
Tests integración
3
Capas (Routes/Services/Repos)
JWT
Auth con rutas protegidas
Arquitectura de la petición
HTTP Request
Next.js / cURL
Netty Engine
embeddedServer
Plugins Pipeline
CORS · Auth · StatusPages · ContentNeg
Routes DSL
/auth · /campaigns · /health
Services
CampaignService · AuthService
Repositories
CampaignRepository · UserRepository
PostgreSQL
Exposed ORM
🔌 Plugins instalados en Application.module()
ContentNegotiation + JSON Authentication JWT StatusPages (5 handlers) CORS (2 hosts) Koin DI WebSockets
fun Application.module() {
  configureSerialization()
  configureAuthentication()
  configureStatusPages()
  configureCORS()
  configureDI()
  configureRouting()
}
🧪 Tests de integración — testApplication
GET /health devuelve status ok
PASS
POST /auth/register crea un usuario nuevo
PASS
POST /campaigns sin JWT devuelve 401
PASS
POST /campaigns con JWT crea campaña (201)
PASS
Presupuesto < 100€ devuelve 400
PASS
// testApplication — sin levantar puerto
testApplication {
  application { module() }
  val res = client.post("/campaigns") {
    bearerAuth(token)
    setBody(CreateCampaignRequest(...))
  }
  res.status shouldBe HttpStatusCode.Created
}
API Endpoints
📋 Rutas definidas — Routing DSL
Método Ruta Descripción Auth
GET /health Estado del servidor + versión
POST /auth/login Login → JWT token
POST /auth/register Registro de nuevo usuario
GET /auth/me Perfil del usuario autenticado JWT
GET /campaigns?page=&limit= Listar campañas (paginado)
POST /campaigns Crear campaña nueva (mín. 100€) JWT
GET /campaigns/{id} Obtener campaña por ID
PATCH /campaigns/{id}/status Cambiar estado (DRAFT→ACTIVE→PAUSED) JWT
DELETE /campaigns/{id} Archivar / eliminar campaña JWT
Modelos de datos — @Serializable
📦 Domain models con kotlinx.serialization
Campaign
idString
nameString
channelCampaignChannel
budgetEurDouble
statusCampaignStatus
createdAtInstant
ownerIdString
ApiResponse<T>
successBoolean
dataT?
errorString?
ApiResponse.ok(data)
ApiResponse.error(msg)
PaginatedResponse<T>
dataList<T>
totalLong
pageInt
limitInt
Enums
CampaignChannel
EMAIL · SOCIAL · SEARCH · DISPLAY
CampaignStatus
DRAFT · ACTIVE · PAUSED · ARCHIVED
Inyección de dependencias — Koin
💉 di/AppModule.kt + configureDI()
val appModule = module {
  // Repositorios — interfaces → implementaciones
  single<CampaignRepository> { InMemoryCampaignRepository() }
  single<UserRepository> { InMemoryUserRepository() }

  // Servicios — inyectan repositorios automáticamente
  single { CampaignService(get()) }
  single { AuthService(get(), get<AppConfig>()) }
  single { AppConfig(jwtSecret = "super-secret-key-prod") }
}

// En routes — inyección lazy
fun Route.campaignRoutes() {
  val campaignService by inject<CampaignService>()
  // ...
}
Manejo de errores — StatusPages
🛡 Handlers centralizados para toda la aplicación
Excepción HTTP Status Mensaje
AuthenticationException 401 Autenticación requerida
AuthorizationException 403 Acceso denegado
NotFoundException 404 Recurso no encontrado
IllegalArgumentException 400 cause.message (validación)
Throwable (catch-all) 500 Error interno del servidor