Backend con Swift
Backend en Swift

Backend con Swift ¡Creamos una web con Vapor, Bootstrap y Heroku!

Crea tu propio backend con Swift, tan solo tienes que usar el Framework Vapor. Hoy aprendemos a crear nuestro backend completamente en Swift, realizamos peticiones HTTP a un servidor de 3ros, creamos leafs y añadimos Bootstrap para darle estilo, y finalmente deployamos en Heroku!

SwiftBeta

Tabla de contenido

Backend con Swift con VAPOR

Hoy en SwiftBeta vamos a:

  • Crear una web usando el framework Vapor
  • Backend completamente en Swift, donde accederemos a una API de 3ros para extraer información y poderla mostrar en nuestra web con HTML
  • Bootstrap para dar estilo a nuestra web
  • Finalmente, deployaremos nuestra web en Heroku (todo completamente gratis).

Va a ser muy sencillo, pero vas a entender conceptos que he ido aprendiendo a lo largo de las últimas dos semanas. Hace unos días creé swiftbeta.herokuapp.com una web donde cada X tiempo irán apareciendo preguntas sobre el entorno Apple, tanto de Swift, SwiftUI, etc. Es un QUIZ donde cualquier puede colaborar y añadir una pregunta para que aparezca en la web, tan solo debe añadirla en un JSON que tengo en un repositorio de Github. (explicar estructura de la API, JSON)

La web que vamos a crear va a usar datos de la API de Rick and Morty.

Web que crearemos con VAPOR
Web que crearemos con VAPOR

Homebrew, Instalar el gestor de paquetes para macOS

Lo primero de todo es instalar la Toolbox de Vapor en nuestro mac. Si no tienes instalado Homebrew recomiendo que lo instales (y si ya lo tienes instalado ves directamente a la siguiente sección), tan solo debes ir al siguiente link:

Homebrew
The Missing Package Manager for macOS (or Linux).

y una vez allí copiar y pegar la URL que aparece para instalarlo, esta URL debes pegarla en tu terminal:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Comando para instalar Homebrew en el terminal

Instalar Vapor

Para instalar la Toolbox de Vapor con Homebrew, lanza este comando en tu terminal:

brew install vapor
Comando para instalar Vapor en el terminal

Creamos nuestro primer proyecto con Vapor

Una vez instalado vapor en nuestro macOS, vamos a crear nuestro primer proyecto. Para hacerlo, lanzamos el siguiente comando en la terminal:

vapor new TutorialSwiftBeta
Crea un proyecto de Vapor

Al ejecutarlo, nos aparecerán varios prompt el primero nos pregunta si queremos usar Fluent, indicamos que no, ya que no vamos a utilizar ninguna base de datos en este tutorial.
El siguiente prompt nos indica si vamos a usar Leaf, en este caso indicamos yes, ya que al hacerlo nos generará nuestro index (los leafs serían como los .html de tu web)

Prompts que aparecen al crear un proyecto con Vapor
Prompts que aparecen al crear un proyecto con Vapor

Al hacerlo, se crea nuestra estructura del proyecto de Vapor con todos los ficheros necesarios:

Proyecto creado con Vapor
Proyecto creado con Vapor

Abrimos el proyecto en Xcode

Ahora, sin salir de la terminal, vamos a entrar dentro de nuestro proyecto. En mi caso voy a ejecutar dos comandos, uno para entrar al proyecto y otro para abrirlo con Xcode:

cd TutorialSwiftBeta
Accedemos al proyecto que acabamos de crear en Vapor

y luego

open Package.swift
Abrimos Xcode ejecutando el comando open

Al hacerlo, se abrirá Xcode. Hay que esperar un rato a que se instalen todas las dependencias del proyecto. Una vez hecho, veremos una estructura de nuestro proyecto como muestra la siguiente imagen:

Proyecto creado en Vapor
Proyecto creado en Vapor

Compilamos el proyecto recién creado

Una vez hemos creado el proyecto y se han instalado las dependencias correctamente, vamos a compilar a ver qué ocurre.

Al hacerlo, vemos por consola que aparece información. Esta información nos servirá para orientarnos de todo lo que ocurre en nuestro servidor. En nuestro caso aparece un mensaje:

[ NOTICE ] Server starting on http://127.0.0.1:8080
Mensaje que aparece en la consola de Xcode

Ahora, podemos abrir Safari (o vuestro explorador preferido) y poner la siguiente dirección localhost:8080. Al hacerlo, vemos que nos aparece un error

Error al intentar abrir la dirección localhost:8080
Error al intentar abrir la dirección localhost:8080

Este error es debido a que no puede encontrar la carpeta con los recursos de nuestra Web App. Para solucionarlo, nos vamos a nuestro target, y le damos a editar

Editamos el Scheme de nuestro proyecto
Editamos el Scheme de nuestro proyecto

Al darle a editar, nos vamos a la sección de Run y allí dentro, en Options, buscamos Working Directory, tal y como te muestro en la siguiente imagen:

En la sección Run vamos a Options
En la sección Run vamos a Options

Aquí dentro, vamos a seleccionar la ruta de nuestro proyecto. Marcamos la opción de Use custom working directory y añadimos la ruta de nuestro proyecto (en mi caso he creado el proyecto en el escritorio)

Ponemos la ruta en la opción Working Directory
Ponemos la ruta en la opción Working Directory

Si ahora volvemos a compilar, quizás os aparece un mensaje como el siguiente, tan solo debéis darle a OK

Ponemos la ruta en la opción Working Directory
Aceptamos el alert que nos muestra el sistema

y ahora sí, vuelves al explorador (Safari, Chrome, etc) y refrescas. Verás que ahora no te aparece el error, sino que aparece el siguiente mensaje:

Al acceder a localhost:8080 vemos el siguiente resultado
Al acceder a localhost:8080 vemos el siguiente resultado

Estructura del proyecto

Tenemos varios ficheros que se han creado al crear el proyecto, pero nos vamos a centrar en:

  • Routes
  • Leafs
  • Controllers

Si vamos al fichero de routes.swift, vemos que ya hay dos rutas creadas. Esta rutas son las que colocamos al lado de la dirección de nuestra web, en este caso podemos probar:

  • localhost:8080 en este caso estamos retornando un EventLoopFuture<View>. Fíjate que al index.leaf, le estamos pasando un diccionario. Esta key, si abrimos index.leaf, vemos que se usa para mostrar el title y el tag h1 de nuestro "HTML"
  • localhost:8080/hello en este caso estamos retornando una simple String. No le pasamos esta información a ningún fichero con extensión leaf, directamente lo mostramos en nuestro navegador.

Creamos nuestra primera ruta

Lo que vamos hacer a continuación es borrar el contenido de la función routes, ya que vamos a crear un controlador que va a contener esta lógica. Una vez borrado, nos vamos a la carpeta de Controllers, y vamos a crear un fichero llamado WebController

Creamos nuestro WebController en Vapor
Creamos nuestro WebController en Vapor

Dentro de este fichero, vamos a crear la siguiente struct:

import Foundation
import Vapor

struct WebController: RouteCollection {
    func boot(routes: RoutesBuilder) throws {
        
    }
}
WebController con las rutas que usaremos en nuestra web

Dentro del método boot, vamos a añadir las rutas que queremos que tenga nuestra app. En nuestro caso solo va a tener una ruta, y la vamos a llamar info

    func boot(routes: RoutesBuilder) throws {
        routes.get("info") { request in
            // TODO:
        }
    }
Ruta "info"

Cuando un user quiera navegar a esta ruta, nosotros lo que haremos será ejecutar cierta lógica. Esta lógica será:

  • Realizar una petición HTTP a un servidor,
  • Recibiremos un JSON
  • Lo mapearemos a un modelo de nuestro domino
  • El modelo lo enviaremos a un leaf para que el user pueda visualizarlo como si fuera un html

Esto es exactamente lo que hago en swiftbeta.herokuapp.com, pero en lugar de ir a buscar los datos a uno de mis repositorios. Lo que vamos hacer es ir a buscarlos a otra API, por ejemplo vamos a probar de usar la de Rick and Morty

Pues vamos a rellenar el TODO: de nuestra nueva ruta info. Aquí es donde pasa toda la magia de nuestro backend. Para hacerlo, vamos a crear dos structs para recuperar la información que queremos del JSON, también vamos a crear una función privada que se encargará de ejecutarse cada vez que se llame a esta ruta

struct ResultsDataModel: Codable {
    let results: [CharacterDataModel]
}

struct CharacterDataModel: Codable {
    let id: Int
    let name: String
    let image: String
}
DataModel del JSON que obtendremos al hacer una petición HTTP

Nuestro WebController quedaría de la siguiente manera:

import Vapor

struct WebController: RouteCollection {
    let uri = URI(string:"https://rickandmortyapi.com/api/character")
    
    func boot(routes: RoutesBuilder) throws {
        routes.get("info", use: getInfo)
    }
    
    private func getInfo(_ req: Request) async throws -> View {
        let context: EventLoopFuture<ResultsDataModel> = req.client.get(uri).flatMapThrowing { response -> Data in
            guard response.status == .ok else {
                throw Abort(.notFound)
            }
            guard let buffer = response.body,
                    let data = String(buffer: buffer).data(using: .utf8) else {
                throw Abort(.badRequest)
            }
            return data
        }.map { data in
            let jsonDecoder = JSONDecoder()

            do {
                let dataModel = try jsonDecoder.decode(ResultsDataModel.self, from: data)
                return dataModel
            } catch {
                return ResultsDataModel(results: [])
            }
        }
        
        let dataModel = try await context.get()
        print(dataModel)
        
        return try await req.view.render("index", dataModel)
    }
}
Lógica para realizar la petición, parsear y pasar a index.leaf

Antes de compilar, debemos añadir estas rutas de alguna manera a nuestra app. Para hacerlo nos vamos al fichero routes.swift. Allí, instanciamos nuestro WebController y lo registramos en nuestra app:

import Vapor

func routes(_ app: Application) throws {
    let webController = WebController()
    try app.register(collection: webController)
}
Añadimos las rutas de nuestra app

Vamos a compilar, y vamos a ver qué ocurre en la consola de nuestro backend (hemos añadido un print para ver que recibimos y parseamos correctamente la petición HTTP).
Al compilar vemos que hemos recibido perfectamente nuestra petición HTTP de la API de Rick and Morty:

ResultsDataModel(results: [App.CharacterDataModel(id: 1, name: "Rick Sanchez", image: "https://rickandmortyapi.com/api/character/avatar/1.jpeg"), App.CharacterDataModel(id: 2, name: "Morty Smith", image: "https://rickandmortyapi.com/api/character/avatar/2.jpeg"),
// ETC
Resultado al realizar la petición HTTP a la API de Rick and Morty

Pero si te fijas, no se está viendo absolutamente nada en nuestro index.leaf, es decir, nuestro explorador nos muestra una web en blanco. Lo que vamos hacer ahora es modificar nuestro index.leaf para que sepa como recoger el dataModel que le estamos pasando y lo sepa visualizar en una tabla.

Abrimos Visual Studio code

En este caso, para modificar el leaf, voy abrir Visual Studio Code. tengo instalada una extensión llamada vapor-leaf que me resalta el código en ficheros con extensión .leaf. Recomiendo que instales alguna de ellas (hay varias) de esta manera te facilitará el modificar este fichero.

Modificamos el index.leaf
Modificamos el index.leaf

Una vez tenemos nuestro index.leaf, vamos a añadir una tablar de HTML, y en los leaf podemos utilizar if, for,etc para poder aplicar lógica o bucles. En este caso vamos a añadir el siguiente código:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">

  <title>#(title)</title>
</head>

<body>
  <h1>#(title)</h1>
  <table>
    <tr>
      <th>Image</th>
      <th>ID</th> 
      <th>Name</th>
    </tr>
    #for(result in results):
    <tr>
      <td>#(result.image)</td>
      <td>#(result.id)</td> 
      <td>#(result.name)</td>
    </tr>
    #endfor
  </table>
</body>
</html>
Código HTML de nuestra web en Vapor

si compilamos nuestra app y refrescamos el navegador, vemos la siguiente tabla:

Resultado obtenido al obtener la información de la API y mostrarla en HTML
Resultado obtenido al obtener la información de la API y mostrarla en HTML

En lugar de ver la URL de la imagen, vamos a ver que se vea la imagen. Para hacerlo es muy simple, modificamos el contenido del for

#for(result in results):
    <tr>
      <td><img src="#(result.image)" alt="#(result.name)"></td>
      <td>#(result.id)</td> 
      <td>#(result.name)</td>
    </tr>
#endfor
Modificamos el HTML para mostrar una imagen

Si compilamos, vamos a ver el cambio que pega nuestra web:

Resultado de nuestra web app usando Vapor
Resultado de nuestra web app usando Vapor

Integración con Bootstrap

Lo último que vamos a ver es cómo añadir Bootstrap a nuestra web. De esta manera podremos sacar ventaja, y tener un estilo añadiendo muy poco código.

En el header añadimos el CSS:

    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
Añadimos CSS Bootstrap

Antes de cerrar el body añadimos:

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
Añadimos Script Bootstrap

Vamos a añadir también un div que sea el container de la table, y vamos añadir varias clases para dar el aspecto que queremos a nuestra table:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">

  <title>#(title)</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
</head>

<body>
  <h1>#(title)</h1>
  <div class="container">
  <table class="table table-responsive table-dark table-striped table-bordered">
    <tr>
      <th>Image</th>
      <th>ID</th> 
      <th>Name</th>
    </tr>
    #for(result in results):
    <tr>
      <td><img src="#(result.image)" alt="#(result.name)"></td>
      <td>#(result.id)</td> 
      <td>#(result.name)</td>
    </tr>
    #endfor
  </table>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>
Resultado final de nuestro index.leaf

Una vez tenemos nuestra app creada, hay que publicarla en algún sitio. Para hacerlo, vamos a usar Heroku, ya verás como es muy simple lanzar a producción tu nueva app.

Deployar nuestra web de Vapor a producción con Heroku

Lo primero de todo que necesitas es tener una cuenta creada en heroku.com, solo tienes que rellenar el típico formulario de registro. Una vez creada nos vamos a nuestro terminal de macOS y en la raíz de nuestro proyecto ejecutamos la siguiente instrucción para instalar heroku en nuestra máquina (para ello vamos a utilizar Homebrew):

brew tap heroku/brew && brew install heroku
Instalamos heroku

Desde la raíz de nuestro proyecto de Xcode jecutamos en nuestro terminal:

heroku login
Login con Heroku

aparecerá un prompt para que hagamos el login en un explorador

Al ejecutar heroku login se abre el explorador
Al ejecutar heroku login se abre el explorador
Explorador abierto para hacer login con Heroku
Explorador abierto para hacer login con Heroku

Hacemos login con la web que se acaba de abrir, y automáticamente ya tendremos una sesión iniciada. El mensaje de nuestra terminal cambiará a:

Login con Heroku OK
Login con Heroku OK

Ahora ya podemos ejecutar varios comandos para crear nuestra app en heroku y deployar el código. Por lo tanto, lo siguiente que vamos a ejecutar en nuestra terminal va a ser:

heroku create tutorialswiftbeta --region eu
Creamos nuestra app en Heroku

Al hacerlo, se creará una aplicación en heroku (estará vacía)

Comando para crear app en Heroku
Comando para crear app en Heroku

Fíjate que ya tenemos una URL asignada para nuestra web, en nuestro caso es https://tutorialswiftbeta.herokuapp.com, si la abrimos vemos un mensaje por defecto de Heroku. Ahora, vamos al momento más esperado, vamos a deployar nuestro código!

Vamos a conectar nuestro repositorio local (si no lo tienes inicializado ejecuta git init en el terminal). El repositorio local (todo nuestro código) lo vamos a conectar con la app que acabamos de crear en Heroku, para ello ejecutamos el siguiente comando en nuestra terminal:

heroku git:remote -a tutorialswiftbeta
Conectamos el git de nuestro proyecto con nuestra app de Heroku
Comando para crear conectar git con nuestra app en Heroku
Comando para crear conectar git con nuestra app en Heroku

Y ahora, vamos a crear un fichero dentro de nuestro proyecto de Xcode. Nos vamos a Xcode, y creamos un Procfile, y dentro de este fichero solo vamos a añadir una línea:

web: Run serve --env production --hostname 0.0.0.0 --port $PORT
Creamos el fichero Procfile

Para que quede más claro te muestro una captura del fichero con su contenido

Xcode con el fichero Procfile necesario para deployar en Heroku
Xcode con el fichero Procfile necesario para deployar en Heroku
Puedes encontrar más información del Procfile en este enlace de la documentación de Vapor https://docs.vapor.codes/3.0/deploy/heroku/#procfile

Ahora haz un comit de tus cambios

git add .
git commit -m "First Commit"
Commiteamos todos los cambios

Una comiteado, vamos a instalar una última dependencia que vamos a necesitar justo ahora.

heroku buildpacks:set vapor/vapor
Instalamos la última dependencia para poder deployar Vapor en Heroku

Y por fín, ejecuta en el terminal

git push heroku master
Pusheamos (deployamos) en Heroku nuestra app
Inicio del Deploy
Inicio del Deploy

Cuando acabe verás algo parecido a:

Fin del deploy
Fin del deploy

Este proceso puede tardar unos minutos, pero cuando acabe te indicará que ya puedes acceder a la misma URL que aparecía en uno de los pasos anteriores.

tutorialswiftbeta.herokuapp.com

Conclusión

Hoy hemos aprendido a cómo crear nuestra primera web app usando el framework VAPOR. Con muy pocas líneas de código hemos sacado algo muy interesante, y con muy pocos retoques puedes crear apps muy potentes. También hemos usado Bootstrap para darle un aspecto más atractivo a nuestra web y finalmente hemos deployado nuestros cambios en Heroku.

Si quieres seguir aprendiendo sobre SwiftUI, Swift, Xcode, o cualquier tema relacionado con el ecosistema Apple