Swift en el servidor es una realidad productiva gracias a Vapor, el framework web más completo del ecosistema. Funciona en Linux y macOS, tiene un rendimiento comparable a Go en benchmarks de concurrencia, y aprovecha el sistema de tipos de Swift para hacer que errores comunes en otros frameworks sean imposibles en tiempo de compilación. Este artículo cubre el ciclo completo: rutas, controladores, Fluent ORM, middleware y despliegue.
Estructura de un proyecto Vapor
vapor new MiAPI --template api
cd MiAPI
swift run
La estructura básica:
// Sources/App/configure.swift
import Vapor
import Fluent
import FluentPostgresDriver
public func configure(_ app: Application) async throws {
// Base de datos
app.databases.use(
.postgres(configuration: .init(
hostname: Environment.get("DB_HOST") ?? "localhost",
username: Environment.get("DB_USER") ?? "vapor",
password: Environment.get("DB_PASSWORD") ?? "",
database: Environment.get("DB_NAME") ?? "miapi"
)),
as: .psql
)
// Migraciones
app.migrations.add(CrearUsuarios())
app.migrations.add(CrearPosts())
try await app.autoMigrate()
// Rutas
try routes(app)
}
Rutas: definición y agrupación
Las rutas en Vapor son funciones que reciben un Request y retornan algo que conforme a ResponseEncodable:
// Sources/App/routes.swift
func routes(_ app: Application) throws {
// Ruta simple
app.get("health") { req in
["status": "ok"]
}
// Parámetros de ruta
app.get("usuarios", ":id") { req async throws -> Usuario in
guard let id = req.parameters.get("id", as: UUID.self) else {
throw Abort(.badRequest, reason: "ID inválido")
}
guard let usuario = try await Usuario.find(id, on: req.db) else {
throw Abort(.notFound)
}
return usuario
}
// Grupos de rutas con prefijo
let api = app.grouped("api", "v1")
try api.register(collection: UsuariosController())
try api.register(collection: PostsController())
}
RouteCollection: controladores
Los controladores organizan las rutas relacionadas en un tipo que conforma a RouteCollection:
struct UsuariosController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
let usuarios = routes.grouped("usuarios")
usuarios.get(use: listar)
usuarios.post(use: crear)
usuarios.group(":usuarioID") { usuario in
usuario.get(use: obtener)
usuario.put(use: actualizar)
usuario.delete(use: eliminar)
}
// Rutas protegidas con autenticación
let protegidas = usuarios.grouped(TokenAuthMiddleware())
protegidas.get("perfil", use: miPerfil)
}
func listar(_ req: Request) async throws -> [Usuario.Public] {
let pagina = try req.query.decode(Paginacion.self)
let usuarios = try await Usuario.query(on: req.db)
.filter(.$activo == true)
.sort(.$createdAt, .descending)
.paginate(pagina)
return usuarios.items.map { $0.toPublic() }
}
func crear(_ req: Request) async throws -> HTTPStatus {
let entrada = try req.content.decode(Usuario.Create.self)
try Usuario.Create.validate(content: req)
let hash = try req.application.password.hash(entrada.password)
let usuario = Usuario(nombre: entrada.nombre, email: entrada.email, passwordHash: hash)
try await usuario.save(on: req.db)
return .created
}
func obtener(_ req: Request) async throws -> Usuario.Public {
guard let usuario = try await Usuario.find(
req.parameters.get("usuarioID"),
on: req.db
) else {
throw Abort(.notFound)
}
return usuario.toPublic()
}
}
Fluent ORM: modelos y migraciones
Fluent es el ORM de Vapor. Los modelos conforman al protocolo Model:
import Fluent
final class Usuario: Model, Content, @unchecked Sendable {
static let schema = "usuarios"
@ID(key: .id) var id: UUID?
@Field(key: "nombre") var nombre: String
@Field(key: "email") var email: String
@Field(key: "password_hash") var passwordHash: String
@Field(key: "activo") var activo: Bool
@Children(for: .$autor) var posts: [Post]
@Timestamp(key: "created_at", on: .create) var createdAt: Date?
@Timestamp(key: "updated_at", on: .update) var updatedAt: Date?
init() { }
init(nombre: String, email: String, passwordHash: String) {
self.nombre = nombre
self.email = email
self.passwordHash = passwordHash
self.activo = true
}
// DTO para respuestas públicas (sin el hash)
struct Public: Content {
let id: UUID
let nombre: String
let email: String
}
func toPublic() -> Public {
Public(id: id!, nombre: nombre, email: email)
}
}
// Migración
struct CrearUsuarios: AsyncMigration {
func prepare(on database: Database) async throws {
try await database.schema("usuarios")
.id()
.field("nombre", .string, .required)
.field("email", .string, .required)
.unique(on: "email")
.field("password_hash", .string, .required)
.field("activo", .bool, .required, .sql(.default(true)))
.field("created_at", .datetime)
.field("updated_at", .datetime)
.create()
}
func revert(on database: Database) async throws {
try await database.schema("usuarios").delete()
}
}
Middleware de autenticación
struct TokenAuthMiddleware: AsyncMiddleware {
func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
guard let bearerToken = request.headers.bearerAuthorization?.token else {
throw Abort(.unauthorized, reason: "Token requerido")
}
guard let sesion = try await SesionToken.query(on: request.db)
.filter(.$token == bearerToken)
.filter(.$expiracion > Date())
.with(.$usuario)
.first() else {
throw Abort(.unauthorized, reason: "Token inválido o expirado")
}
request.auth.login(sesion.usuario)
return try await next.respond(to: request)
}
}
// Middleware de rate limiting
struct RateLimitMiddleware: AsyncMiddleware {
let limite: Int
let ventana: TimeInterval
func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
let ip = request.remoteAddress?.ipAddress ?? "unknown"
let clave = "ratelimit:(ip)"
let contador = try await request.application.caches.memory.get(clave, as: Int.self) ?? 0
guard contador < limite else {
throw Abort(.tooManyRequests)
}
try await request.application.caches.memory.set(clave, to: contador + 1, expiresIn: .seconds(Int(ventana)))
return try await next.respond(to: request)
}
}
Despliegue en Linux con systemd
# Compilar en modo release
swift build -c release
# Archivo de servicio systemd
# /etc/systemd/system/miapi.service
[Unit]
Description=Mi API en Vapor
After=network.target postgresql.service
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/miapi
ExecStart=/opt/miapi/.build/release/Run serve --env production --port 8080
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
Environment=DB_HOST=localhost
Environment=DB_USER=vapor
Environment=DB_PASSWORD=secreto
Environment=DB_NAME=miapi
[Install]
WantedBy=multi-user.target
systemctl enable miapi
systemctl start miapi
systemctl status miapi
Nginx como proxy inverso
server {
listen 443 ssl;
server_name api.midominio.com;
location / {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400; # Para WebSocket
}
}
Resumen
Vapor proporciona todo lo necesario para una API REST o WebSocket de producción: routing expresivo con RouteCollection, Fluent ORM con migraciones tipadas, middleware para autenticación y rate limiting, y un modelo async/await que escala bien. El despliegue en Linux es sencillo con systemd, y Swift en el servidor comparte código con el cliente iOS, lo que reduce la duplicación de modelos entre frontend y backend.
