Saltar a contenido

Routing

El enrutamiento (o routing) consiste en encontrar el controlador apropiado para una petición entrante. El núcleo del routing de Vapor lo compone el router de tres nodos de alto rendimiento de RoutingKit.

Presentación

Para comprender cómo funciona routing en Vapor, primero debes comprender algunos conceptos básicos sobre las peticiones HTTP (requests). Eche un vistazo a la siguiente petición de ejemplo.

GET /hello/vapor HTTP/1.1
host: vapor.codes
content-length: 0

Esta es una simple petición HTTP GET a la URL /hello/vapor. Este es el tipo de petición HTTP que haría tu navegador si apuntara a la siguiente URL.

http://vapor.codes/hello/vapor

Métodos HTTP

La primera parte de la petición es el método de HTTP. GET es el método más común de HTTP, pero hay varios que usarás con frecuencia. Estos métodos HTTP a menudo se asocian con la semántica CRUD.

Método CRUD
GET Leer
POST Crear
PUT Reemplazar
PATCH Actualizar
DELETE Borrar

Ruta de Petición (Path)

Justo después del método HTTP está la URI (identificador de recursos uniforme) de la petición. Consiste en una ruta que comienza en / y una cadena de consulta opcional detrás de ?. El método HTTP y la ruta son los que usa Vapor para enrutar las peticiones.

Después de la URI está la versión HTTP seguida de cero o más encabezados (headers) y finalmente el cuerpo de la respuesta (body). Dado que se trata de una petición GET, no hay cuerpo.

Métodos de Router

Echemos un vistazo a cómo se podría manejar esta petición en Vapor.

app.get("hello", "vapor") { req in 
    return "Hello, vapor!"
}

Todos los métodos comunes de HTTP están disponibles como métodos en Application. Aceptan uno o más argumentos de cadena que representan la ruta de la petición separados por /.

Ten en cuenta que también podrías escribirlo usando on seguido del método.

app.on(.GET, "hello", "vapor") { ... }

Con esta ruta registrada, la petición HTTP de ejemplo anterior dará como resultado la siguiente respuesta HTTP.

HTTP/1.1 200 OK
content-length: 13
content-type: text/plain; charset=utf-8

Hello, vapor!

Parámetros de Ruta

Ahora que hemos creado una petición de ruta con éxito basada en el método y ruta HTTP, intentemos hacer que la ruta sea dinámica. Ten en cuenta que el nombre "vapor" está codificado tanto en la ruta como en la respuesta. Hagámosla dinámica para que puedas visitar /hello/<cualquier nombre> y obtener una respuesta.

app.get("hello", ":name") { req -> String in
    let name = req.parameters.get("name")!
    return "Hello, \(name)!"
}

Al usar un componente de ruta con el prefijo :, le indicamos al router que se trata de un componente dinámico. Cualquier cadena suministrada aquí ahora coincidirá con esta ruta. Luego podemos usar req.parameters para acceder al valor de la cadena.

Si vuelves a ejecutar la petición de ejemplo, seguirás recibiendo una respuesta que saluda a vapor. Sin embargo, ahora puedes incluir cualquier nombre luego de /hello/ y verlo incluido en la respuesta. Probemos /hello/swift.

GET /hello/swift HTTP/1.1
content-length: 0
HTTP/1.1 200 OK
content-length: 13
content-type: text/plain; charset=utf-8

Hello, swift!

Ahora que comprendemos los conceptos básicos, consultemos cada sección para obtener más información sobre parámetros, grupos y más.

Rutas

Una ruta especifica un controlador de peticiones para un método HTTP y una ruta URI determinados. También puede almacenar metadatos adicionales.

Métodos

Las rutas se pueden registrar directamente en tu Application utilizando varios métodos auxiliares de HTTP.

// responde a GET /foo/bar/baz
app.get("foo", "bar", "baz") { req in
    ...
}

Las estructuras de ruta admiten el retorno de cualquier cosa que sea ResponseEncodable. Esto incluye Content, un closure async, y cualquier EventLoopFuture donde su valor future sea ResponseEncodable.

Puedes especificar el tipo de retorno de una ruta usando -> T antes de in. Esto puede ser útil en situaciones en las que el compilador no puede determinar el tipo de retorno.

app.get("foo") { req -> String in
    return "bar"
}

Estos son los métodos de ruta auxiliares soportados:

  • get
  • post
  • patch
  • put
  • delete

Además de los métodos auxiliares de HTTP, existe una función on que acepta el método HTTP como un parámetro de entrada.

// responde a OPTIONS /foo/bar/baz
app.on(.OPTIONS, "foo", "bar", "baz") { req in
    ...
}

Componente de Ruta

Cada método de registro de ruta acepta una lista variádica de PathComponent. Este tipo es expresable por un string literal y consta de cuatro casos:

  • Constante (foo)
  • Parámetro (:foo)
  • Cualquier cosa (*)
  • Comodín (**)

Constante

Este es un componente de ruta estático. Solo permitirá peticiones con una cadena que coincida exactamente en valor y posición con la especificada en la ruta.

// responde a GET /foo/bar/baz
app.get("foo", "bar", "baz") { req in
    ...
}

Parámetro

Este es un componente de ruta dinámico. Se permitirá cualquier cadena en esta posición. Un componente de ruta de parámetro se especifica con el prefijo :. La cadena que sigue a : será el nombre del parámetro, el cual podrás usar para obtener su valor en la petición.

// responde a GET /foo/bar/baz
// responde a GET /foo/qux/baz
// ...
app.get("foo", ":bar", "baz") { req in
    ...
}

Cualquier cosa

Este es muy similar a parámetro, excepto que el valor se descarta. Este componente de ruta es especificado simplemente con un *.

// responde a GET /foo/bar/baz
// responde a GET /foo/qux/baz
// ...
app.get("foo", "*", "baz") { req in
    ...
}

Comodín

Este es un componente de ruta dinámico que coincide con uno o más componentes. Se especifica usando simplemente **. Cualquier cadena en esta posición o en posiciones posteriores coincidirá con la petición.

// responde a GET /foo/bar
// responde a GET /foo/bar/baz
// ...
app.get("foo", "**") { req in 
    ...
}

Parámetros

Cuando se utiliza un componente de ruta de parámetro (con el prefijo :), el valor de la URI en esa posición se almacenará en req.parameters. Puedes utilizar el nombre del componente de ruta para acceder al valor.

// responde a GET /hello/foo
// responde a GET /hello/bar
// ...
app.get("hello", ":name") { req -> String in
    let name = req.parameters.get("name")!
    return "Hello, \(name)!"
}

Consejo

Podemos estar seguros de que req.parameters.get nunca devolverá nil ya que nuestra ruta incluye :name. Sin embargo, si se está accediendo a parámetros de ruta en un middleware o en código activado por múltiples rutas, deberías manejar la posibilidad de un nil.

Consejo

Si deseas recuperar parámetros de consulta de URL (por ejemplo /hello/?name=foo), necesitas usar la API Content de Vapor para manejar los datos codificados del string de la URL. Consulta la referencia de Content para más detalles.

req.parameters.get también soporta convertir el parámetro a los tipos LosslessStringConvertible automáticamente.

// responde a GET /number/42
// responde a GET /number/1337
// ...
app.get("number", ":x") { req -> String in 
    guard let int = req.parameters.get("x", as: Int.self) else {
        throw Abort(.badRequest)
    }
    return "\(int) is a great number"
}

Los valores de URI que coincidan con un Comodín (**) se guardarán en req.parameters como [String]. Puedes usar req.parameters.getCatchall para acceder a estos componentes.

// responde a GET /hello/foo
// responde a GET /hello/foo/bar
// ...
app.get("hello", "**") { req -> String in
    let name = req.parameters.getCatchall().joined(separator: " ")
    return "Hello, \(name)!"
}

Transmisión de Body

Al registrar una ruta utilizando el método on, puedes especificar cómo se debe manejar la petición de Body (el cuerpo de la respuesta). Por defecto, los cuerpos de las peticiones se recopilan en memoria antes de llamar a su controlador correspondiente. Esto es útil ya que permite que la decodificación del contenido sea síncrona aunque su aplicación lea las peticiones entrantes de forma asíncrona.

Por defecto. Vapor limitará la recopilación de la transmisión del Body a un tamaño de 16KB. Puedes configurar esto usando app.routes.

// Aumenta el límite de recopilación de la transmisión de Body hasta 500kb
app.routes.defaultMaxBodySize = "500kb"

Si una transmisión de Body que se está recopilando excede el límite configurado, se lanzará un error 413 Payload Too Large.

Para configurar la estrategia de recopilación de Body en una ruta individual, usa el parámetro body.

// Recopila la transmisión de Body (hasta 1mb de tamaño) antes de llamar a esta ruta.
app.on(.POST, "listings", body: .collect(maxSize: "1mb")) { req in
    // Administra la petición. 
}

Si se proporciona el maxSize (tamaño máximo) al realizar el collect (la recopilación), se sobrescribirá el valor predeterminado de la aplicación para esa ruta. Para utilizar el valor por defecto de la aplicación, omita el argumento maxSize.

Para peticiones grandes, como subida de archivos, recopilar el Body en un búfer puede sobrecargar la memoria del sistema. Para evitar que se recopile el Body de la petición, usa la estrategia stream.

// El cuerpo de la petición no se recopilará en un búfer.
app.on(.POST, "upload", body: .stream) { req in
    ...
}

Cuando se transmite el cuerpo de la petición, req.body.data será nil. Debes usar req.body.drain para manejar cada fragmento a medida que se envía a tu ruta.

Rutas Case Insensitive

El comportamiento por defecto para las rutas distingue (case-sensitive) y preserva (case-preserving) entre mayúsculas y minúsculas. Los componentes de ruta constantes (Constant) pueden manejarse alternativamente sin distinguir ni preservar entre mayúsculas y minúsculas a efectos de cada ruta; para habilitar este comportamiento, configura lo siguiente antes del inicio de la aplicación:

app.routes.caseInsensitive = true

No se realizarán cambios en la petición de origen; las rutas recibirán los componentes sin ninguna modificación.

Visualizando Rutas

Puedes acceder a las rutas de tu aplicación creando el servicio Routes o utilizando app.routes.

print(app.routes.all) // [Route]

Vapor también posee el comando routes que imprime todas las rutas disponibles en una tabla con formato ASCII.

$ swift run App routes
+--------+----------------+
| GET    | /              |
+--------+----------------+
| GET    | /hello         |
+--------+----------------+
| GET    | /todos         |
+--------+----------------+
| POST   | /todos         |
+--------+----------------+
| DELETE | /todos/:todoID |
+--------+----------------+

Metadata

Todos los registro de métodos de rutas retornan la ruta creada (Route). Esto te permite agregar metadatos al diccionario userInfo de la ruta. Hay algunos métodos predeterminados disponibles, como agregar una descripción a la ruta.

app.get("hello", ":name") { req in
    ...
}.description("says hello")

Grupos de Ruta

La agrupación de rutas permite crear un conjunto de rutas con un prefijo o un middleware específico. La agrupación admite una sintaxis basada en builders y closures.

Todos los métodos de agrupación devuelven un RouteBuilder, permitiendo mezclar, combinar y anidar infinitamente sus grupos con otros métodos de creación de rutas.

Prefijo de Ruta

Los grupos de rutas con prefijo permiten anteponer uno o más componentes de ruta a un grupo de rutas.

let users = app.grouped("users")
// GET /users
users.get { req in
    ...
}
// POST /users
users.post { req in
    ...
}
// GET /users/:id
users.get(":id") { req in
    let id = req.parameters.get("id")!
    ...
}

Cualquier componente de ruta que puedas pasar como get o post puede pasarse dentro de un grouped. También hay una sintaxis alternativa basada en closures.

app.group("users") { users in
    // GET /users
    users.get { req in
        ...
    }
    // POST /users
    users.post { req in
        ...
    }
    // GET /users/:id
    users.get(":id") { req in
        let id = req.parameters.get("id")!
        ...
    }
}

La anidación de grupos de rutas con prefijos te permite definir de manera concisa las APIs de un CRUD.

app.group("users") { users in
    // GET /users
    users.get { ... }
    // POST /users
    users.post { ... }

    users.group(":id") { user in
        // GET /users/:id
        user.get { ... }
        // PATCH /users/:id
        user.patch { ... }
        // PUT /users/:id
        user.put { ... }
    }
}

Middleware

Además de prefijar componentes de rutas, también puedes agregar un middleware a grupos de rutas.

app.get("fast-thing") { req in
    ...
}
app.group(RateLimitMiddleware(requestsPerMinute: 5)) { rateLimited in
    rateLimited.get("slow-thing") { req in
        ...
    }
}

Esto es especialmente útil para proteger subconjuntos de rutas con diferentes middleware de autenticación.

app.post("login") { ... }
let auth = app.grouped(AuthMiddleware())
auth.get("dashboard") { ... }
auth.get("logout") { ... }

Redirecciones

Los redireccionamientos (redirect) son útiles en varios escenarios, como reenviar ubicaciones antiguas a nuevas para el SEO, redireccionar a un usuario no autenticado a la página de inicio de sesión o mantener la compatibilidad con versiones anteriores de su API.

Para redirigir una petición, utiliza:

req.redirect(to: "/some/new/path")

También puedes especificar el tipo de redirección, por ejemplo para redirigir una página de forma permanente (para que su SEO se actualice correctamente) usa:

req.redirect(to: "/some/new/path", redirectType: .permanent)

Los diferentes Redirect son:

  • .permanent - devuelve una redirección 301 Moved Permanently
  • .normal - devuelve una redirección 303 See Other. Este es el valor por defecto de Vapor y le dice al cliente que siga la redirección con una petición GET.
  • .temporary - devuelve una redirección 307 Temporary Redirect. Esto le dice al cliente que conserve el método HTTP utilizado en la petición.

Para elegir el código de estado de redirección adecuado, consulte la lista completa