How to use Vapor Part 2.

Scott
14 min readJul 22, 2023

This is a continuation from How to use Vapor

Type CREATE DATABASE <yourDatabaseName>;

Tap enter

I know it seems sketchy that you have to start by tapping on a different database, in order to create a database.

Lets connect to the newly created database in Vapor

import Fluent
import FluentPostgresDriver

public func configure(_ app: Application) async throws {

app.databases.use(.postgres(hostname: "localhost", username: "postgres", password: "", database: "storiesdb"), as: .psql)

▶️ Run the app

It may be deprecated but I haven’t figured out how to pass the relevant arguments to the alternative.

When you have a new team member, their database will be empty, but when they run Migrations it will add all the necessary tables.

import Fluent
import FluentPostgresDriver

/// Creates a database table called Stories
struct CreateStory: Migration {

private let storiesTableName: String = "stories"

/// Creates the table
func prepare(on database: Database) -> EventLoopFuture<Void> {
database
.schema(storiesTableName) // table name
.id()
.field("title", .string) // column name
.create()
}

/// Undos the table creation
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(storiesTableName).delete()
}
}

Now we can run our migration like so:

import NIOSSL
import Fluent
import FluentPostgresDriver
import Leaf
import Vapor

// configures your application
public func configure(_ app: Application) async throws {
// uncomment to serve files from /Public folder
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))

app.databases.use(
.postgres(
hostname: "localhost",
username: "postgres",
password: "",
database: "storiesdb"
),
as: .psql
)

app.migrations.add(CreateStory()) // <---------

▶️ Run the app, Then

cd to your project in terminal. Then

vapor run migrate

This will take a moment, and eventually it will ask you to confirm running your migrations:

Then it should say “Migration successful”

Lets check out our new table. Open the Postico app.

Tap untitled server, then swap out the database field for your database, then tap connect.

This:

Should match up with:

app.databases.use(
.postgres(
configuration: .init(
hostname: "localhost", // Maps to "Host: localhost"
username: "postgres", // Maps to "User: scottlydon"
password: "", // Maps to "Password: "
database: "akintestdb", // Maps to Database: akintestdb"
tls: PostgresConnection.Configuration.TLS.disable
),
encodingContext: .iso8601,
decodingContext: .iso8601
),
as: .psql

you may be prompted with a permission request.

Tap OK

Should look somewhat like this:

If you don’t see your tables then make sure you follow these steps:

  1. Run your project
  2. Run the terminal command: vapor run migration
  3. Then go to Postico and connect.

Tapping on the stories table, we see it has an id and title like we specified.

You can see the _fluent_migrations which keeps track of the migrations run. Do not make changes to this. It should stay a source of truth for reads.

Don’t add columns via postico, only use the code and migrations so that it will be synced up with the code.

You can tap structure ^ to see the structure.

32. We insert data into the tables by creating Models

▶️ Run the app

33. Go to routes, then:

import Fluent
import Vapor

func routes(_ app: Application) throws {

app.post("stories") { req -> EventLoopFuture<Story> in
let story = try req.content.decode(Story.self) // content = body of request
return story.create(on: req.db).map { story }
}

▶️ Run the app

Now open up postman

Add the header: Content-Type for application/json
For the body we can give it a title.
Tap Send

You should see this in the postman response:

{
"title": "Christopher Robin",
"id": "F292972B-1CC4-4990-94D7-3BD459EC60D2"
}

Open Postico to see your entry. If you don’t see it at first, tap the refresh button.

refresh button
Go back to content

See the entry is there!

33.1. How to add another property to a Model after you have already run the migration.

  • If the app is not in production.

Then you can delete the table in Postico, and run the app and migration in terminal again.

  • If the app is in production.

Then you can make a migration for the new property. For example:

import Fluent

struct AddDescriptionToGoals: Migration {

func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema("goals")
.field("description", .string, .required) // Adjust the data type as needed
.update()
}

func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema("goals")
.deleteField("description")
.update()
}
}

Remember to add it to the routes.

app.migrations.add(AddDescriptionToGoals())

34. How do we get all the stories from the database?

func routes(_ app: Application) throws {

let storiesPath: PathComponent = "stories"

app.get(storiesPath) { req in
Story.query(on: req.db).all()
}

▶️ Run the app

This will get all the stories in the table in the database. To

    let storyId: String = "storyId"

app.get(storiesPath, ":\(storyId)") { req -> EventLoopFuture<Story> in
Story.find(req.parameters.get(storyId), on: req.db)
.unwrap(or: Abort(.notFound))
}

▶️ Run the app

To test it, copy the id of the entry you want to get from Postico. Then pass the following url into your browser:

http://127.0.0.1:8080/stories/<yourItemId>

You should get the object you requested back.

If you get “not found”

  • Make sure the colon is in the get argument but not the find argument.
  • Make sure you ran the app without breakpoints.
  • Make sure you copy and pasted the whole id without leaving out a character.

35. Update an existing story

So we will create an endpoint that uses the Put method. With that, we don’t actually have to pass the id as an argument to the put method, or in the endpoint. We just need to include it in the body of the put request. Like so:

    // /movies PUT
app.put(storiesPath) { req -> EventLoopFuture<HTTPStatus> in
let story = try req.content.decode(Story.self)
return Story.find(story.id, on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap {
$0.title = story.title
return $0.update(on: req.db).transform(to: .ok)
}
}

▶️ Run the app

Then go to postman and send the put request.

36. You can delete a row like so:

    // /stories DELETE
app.delete(storiesPath, ":\(storyId)") { req -> EventLoopFuture<HTTPStatus> in
Story.find(req.parameters.get(storyId), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { $0.delete(on: req.db) }
.transform(to: .ok)
}

▶️ Run the app

To test it, copy the id from postico, then you can go to postman and paste it in the url then set it to delete and tap send. Then check in Postico that it is gone with a refresh.

Copy the id
paste it in the url, then tap Send. It should return an 200 OK
Now you can see that it is gone

37. One to many relationships:

If you have stories and reviews, where each story can have multiple reviews, if you were to put the reviews in the story table, you would encounter an issue where you would have rows with repeating movie names, and uncertainty about what to put for the primary keys because you can’t have those repeat. However you can’t very well put the review ids into an array in a cell in the table because cells don’t accept lists.
The answer is to put the story ids (which there are one of per review) in a column in the review table.

38. Lets create the object for which there will be multiple (many) per story.

import Vapor
import Fluent
import FluentPostgresDriver

final class Review: Model, Content {

static let schema: String = "reviews"

@ID(key: .id)
var id: UUID?

@Field(key: "title")
var title: String

@Field(key: "body")
var body: String

@Parent(key: "story_id")
var story: Story

init() {}

init(
id: UUID? = nil,
title: String,
body: String,
storyId: UUID
) {
self.id = id
self.title = title
self.body = body
self.$story.id = storyId
}
}

▶️ Run the app

39. Create a Migration for the object:

import Vapor
import Fluent
import FluentPostgresDriver

/// Adds a Review table
struct CreateReview: Migration {

func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema(Review.schema)
.id()
.field("title", .string)
.field("body", .string)
.field("story_id", .uuid, .references(Story.schema, "id"))
.create()
}

func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(Review.schema).delete()
}
}

▶️ Run the app

Now add the migration in the configure file:

app.migrations.add(CreateReview())

▶️ Run the app

Open terminal and run the migration (vapor run migrate):

➜  DeleteMe2 git:(main) ✗ vapor run migrate

Then open postico to check that the table was added.

You can see the reviews table was added.
Tap the structure
You should see the story here for the one to many relationship

40. Adding Model relationships:

The following demonstrates a one to many relationship


import Fluent
import Vapor

final class Story: Model, Content {

// ...

@Children(for: \.$story)
var reviews: [Review]

▶️ Run the app

And

import Vapor
import Fluent
import FluentPostgresDriver

final class Review: Model, Content {

// ...

@Parent(key: "story_id")
var story: Story

▶️ Run the app

41. How to save an entry (Review)

import Fluent
import Vapor

func routes(_ app: Application) throws {

// ...

app.post("\(Review.schema)") { req -> EventLoopFuture<Review> in
let review = try req.content.decode(Review.self)
return review.create(on: req.db).map { review }
}

▶️ Run the app

Copy the story id from postico
Setup the postman request, pass the id to the story property.

Change the URL to reviews.

Remember to run your project to update the instance!

Your response should look something like this, notice the new review id in the response.

Open up Postico to check, remember to refresh.

42 Retrieving all the reviews per story (all the many given a one)

import Fluent
import Vapor

func routes(_ app: Application) throws {

// ...

app.get("\(Story.schema)") { req in
// Gets all the stories and reviews associated with each story
Story.query(on: req.db).with(\.$reviews).all()
}

▶️ Run the app

Now if we change the call to a get and pass stories as the path, send it, we should receive a story with all of its reviews. In our case, there is only one.

— — — — — - side note

I’m taking a vapor course and I learned how to use with( for a many to many get call. For actors to movies, however, suppose I wanted to get an object property on a movie type as well, for example i want actors, and for each actor I want a list of each movie they’ve been in, and for each movie I want the directors that directed each movie, what would the route code look like?

— — — — — answer

Vapor, a server-side web framework for Swift, leverages Swift’s strong type safety and the Fluent ORM for handling database interactions. However, this question is quite complex, as it involves multiple layers of relationships (many-to-many and one-to-many). The following code snippet is an abstracted example of how you might achieve this with Vapor. This will need to be adapted to your specific database schema and model relationships.

router.get("actors", use: { req -> Future<[Actor]> in
return Actor.query(on: req)
.with(\.$movies) { movie in
movie.with(\.$directors)
}
.all()
}
)

▶️ Run the app

This route handler will return all actors along with their associated movies and the directors of each movie.

The .with(\.$movies) function is used to fetch all the associated movies for each actor, while the inner .with(\.$directors) function fetches all the directors associated with each movie.

Please note that you need to have defined the appropriate relationships in your models. In this case, you would have a pivot between `Actor` and `Movie`, and a one-to-many relationship from `Movie` to `Director`.

The code above is written assuming Vapor 4's syntax, which had some changes from Vapor 3, especially around the database and routing APIs.

Remember to handle all potential errors and edge cases in your production code, as this example is just a simplified scenario. Always test your code thoroughly, considering different potential inputs and edge cases to ensure it behaves as expected.

43. Many to Many

In order to manage a many to many relationship we must create a table purely dedicated to joining two tables, also known as a “Join” or “Pivot.”

Because a story can be “co-authored”, while an author can write multiple stories. We will use authors and stories to demonstrate many to many relationships.

44. Adding Author Model

import Vapor
import Fluent
import FluentPostgresDriver

final class Author: Model, Content {
static let schema: String = "authors"

@ID(key: .id)
var id: UUID?

@Field(key: "name")
var name: String

init() {}

init(id: UUID? = nil, name: String) {
self.id = id
self.name = name
}
}

▶️ Run the app

45. Adding pivot Model

Notice the pivot just has the id and reference to the first and the second model via their ids.


import Vapor
import Fluent
import FluentPostgresDriver

final class StoryAuthor: Model {
static let schema: String = "story_actors"

@ID(key: .id)
var id: UUID?

@Parent(key: "story_id")
var story: Story

@Parent(key: "author_id")
var author: Author

init() {}

init(storyId: UUID, authorId: UUID) {
self.$story.id = storyId
self.$author.id = authorId
}
}

▶️ Run the app

46. Create Migrations


import Fluent
import FluentPostgresDriver

struct CreateAuthor: Migration {

func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema(Author.schema)
.id()
.field("name", .string)
.create()
}

func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(Author.schema).delete()
}
}

And the join/pivot migration:

import Fluent
import FluentPostgresDriver

struct CreateStoryAuthor: Migration {

func prepare(on database: Database) -> EventLoopFuture<Void> {
database
.schema(StoryAuthor.schema)
.id()
.field("story_id", .uuid, .required, .references("stories", "id"))
.field("author_id", .uuid, .required, .references("actors", "id"))
.create()
}

func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(StoryAuthor.schema).delete()
}
}

Then call the migrations, WARNING: make sure you call the create the tables that the joins reference before calling the Join migration!

    app.migrations.add(CreateAuthor())
app.migrations.add(CreateStoryAuthor())

▶️ Run the app

Then go to terminal cd to your project, then

vapor run migrate

And say y to the prompt.

We can open up Postico to see that they were created.

hopefully they were.
The structure should look something like this.

47. Adding relationships in Story and Author Models

Siblings is used for many to many relationships.

import Fluent
import Vapor

final class Story: Model, Content {

@Siblings(through: StoryAuthor.self, from: \.$story, to: \.$author)
var authors: [Author]

And Author:

import Vapor
import Fluent
import FluentPostgresDriver

final class Author: Model, Content {

@Siblings(through: StoryAuthor.self, from: \.$author, to: \.$story)
var stories: [Story]

▶️ Run the app

48. Creating a new Story and Author:

import Fluent 
import Vapor

func routes(_ app: Application) throws {

app.post("\(Author.schema)") { req -> EventLoopFuture<Author> in
let author = try req.content.decode(Author.self)
return author.create(on: req.db).map { author }
}

▶️ Run the app

Trying it in Postman
Confirming in Postico

49. Attaching authors to stories, many to many relationships.

import Fluent
import Vapor

func routes(_ app: Application) throws {

// story/:storyId/author/:authorId
app.post("story", ":storyId", "author", ":authorId") { req -> EventLoopFuture<HTTPStatus> in

// get the story
let story = Story.find(req.parameters.get("storyId"), on: req.db)
.unwrap(or: Abort(.notFound)) // returns a promise that it will be returned eventually.

// get the author
let author = Author.find(req.parameters.get("authorId"), on: req.db)
.unwrap(or: Abort(.notFound))

return story.and(author).flatMap { story, author in
story.$authors.attach(author, on: req.db)
}.transform(to: .ok)
}

▶️ Run the app

Open up Postico and grab a valid author id and a valid story id.

id for the story
Id for the author

Then open up Postman and send the post:

Then check Postico to see the entry

Tada! its there. 🎊

— — — — — — — — — - side note, question to GpT

So I made a pivot table, however, I’m not sure how the pivot table links to or associates with given properties. For example suppose I had two properties on a Movie: leadActors: [Actor] and backupActors: [Actor] how would my pivot table differentiate between the actors associated to a movie for one property vs another?

— — — — — — — — — GpT answer

To differentiate between different types of relationships (e.g., leadActors and backupActors), you would typically create two separate pivot tables. Each pivot table would represent a distinct many-to-many relationship.

In Swift Vapor, each pivot table is represented by a model. For example, you might have a `LeadActorMoviePivot` and a `BackupActorMoviePivot`. Both models would contain foreign keys referencing the `Actor` and `Movie` models, but would be used in different contexts according to the relationship they represent.

Your `Author` model might look something like this:

final class Actor: Model {
static let schema = "actors"
// Properties…
var leadMovies: Siblings<Actor, Movie, LeadActorMoviePivot> {
siblings(through: LeadActorMoviePivot.self)
}

var backupMovies: Siblings<Actor, Movie, BackupActorMoviePivot> {
siblings(through: BackupActorMoviePivot.self)
}
}

▶️ Run the app

And your `Story` model:

final class Movie: Model {
static let schema = "movies"
// Properties…
var leadActors: Siblings<Movie, Actor, LeadActorMoviePivot> {
siblings(through: LeadActorMoviePivot.self)
}
var backupActors: Siblings<Movie, Actor, BackupActorMoviePivot> {
siblings(through: BackupActorMoviePivot.self)
}
}

▶️ Run the app

Finally, you would define your pivot models like this:

final class LeadActorMoviePivot: Model {
static let schema = "lead_actor_movie"
// Properties…
}
final class BackupActorMoviePivot: Model {
static let schema = "backup_actor_movie"
// Properties…
}

▶️ Run the app

This way, you can differentiate between lead and backup actors in your database and in your Vapor code. Just remember to adjust this sample code according to your actual model properties and database schema.

Remember, it’s important to maintain good database practices such as enforcing referential integrity and understanding how to use indexes for better performance. Ensure your database schema is designed properly to minimize redundancy and optimize performance.

50. Time to fetch the many to many relationship.

    app.get("\(Author.schema)") { req in
Author.query(on: req.db).with(\.$stories).all()
}

▶️ Run the app

pThat shows us stories associated with each author.

If we want to get authors associated with all the stories, we can add the same with( call to the return for the stories route.

    app.get("\(Story.schema)") { req in
// Gets all the stories and reviews associated with each story
Story.query(on: req.db).with(\.$authors).with(\.$reviews).all()
}

▶️ Run the app

See the authors included?

--

--