Episode #577

Routing and Controllers

Series: Build a Vapor Backend

18 minutes
Published on May 16, 2024

This video is only available to subscribers. Get access to this video and 584 others.

In this episode, we explore Vapor routing more deeply. We set up a route to fetch a band by its slug, handling async operations and errors. After testing, we refactor with a findBySlug method for reusability. We add a route to fetch songs for a band and discuss avoiding inefficient querying. To organize the code, we create BandsController and SongsController to group routes.

This episode uses Swift 5.10, Neovim 0.9.5, Vapor 4.92.

In this episode, we're going to take a look at routing.

And I have the application here open in the terminal.

I'm running the database with Docker here.

And just to change things up a bit,

I'm going to use Vim today to edit our source files.

And I have LSP set up so that we should

be able to get some syntax highlighting and completion

and stuff.

So we talked a little bit last time about routing.

We used these routes here, which were very basic.

And I'm just going to delete these.

We can see that this is doing a get request on the application.

And each route is handed the request.

It can be async and throws, optionally.

And it can return values.

So I'm going to delete this.

And we're going to start writing our first route

for our application, which is going to be

showing the band by a slug.

So we're going to say app.get.

And app.get is going to take a band.

And then we are going to separate the path components

like this.

And the way we pass arguments here is with a colon.

So that's going to give us our request.

Because we are going to talk to a database,

this is going to be async throws.

And it can infer the return type for us.

So we don't have to specify it.

So then inside of here, we need to get at that parameter slug.

And so this is going to match something like localhost 8080

slash band slash retro rewind.

And so we need to get access to that variable.

And the way we do that is by saying request.parameters.

And parameters, you have get, which returns the name.

But it's optional.

And then we also have require, which throws.

And in this case, we want require.

So I want to get the slug parameter.

And because that throws, we need to run that in with try.

So next, we need to find the band.

And so we're going to query.

We're going to talk a little bit more about querying as we go.

But what we're going to do here is find the band.

We're going to say query on database.

We get that from request.db.

Then what we can do here is filter.

And the filters are kind of magic.

It takes a key path to a property wrapper

on the band instance.

And so our property wrapper is $slug.

And then we're going to use equal equal.

And that's going to be slug.

So this is going to give me all the bands that

match the slug.

We can say limit 1, which is going

to limit the number of results coming back from the database,

to 1.

And then we'll get the first one.

Now, this is throwing and async.

So we're going to need to do that.

And now that we have our band, we're

going to need to check to see if that band exists.

And if it does, we could say guard let band.

Otherwise, we're going to throw an error.

And the way that we halt a request

is with the abort error.

And then we can pass in an error code of not found.

And then I'm just going to return the band.

So that's our first route.

I'm going to go over to another tab here.

And we're going to run Swift run app.

And now our application is running.

And now I'm going to do curl -i.

We're going to use HTTP localhost 8080 band slash.

And I'm just going to type something

that I know doesn't exist.

And we get a 404 not found.

We get the error response here that

says the reason was not found.

And we can also see that in the server logs up here.

If we take a look at the database--

I'm going to look at that real quick--

we can see that it actually did do the query.

And it passed that detail parameter as ASDF.

OK, so that's good.

Let's look for retro rewind, which

is a band in the database.

And we can see that we get the JSON response from that.

And it automatically does this for us.

So there may be some things that we want to change about this.

For instance, we may want to use a different JSON encoder.

We can talk about that a little later.

We also may want to change what values we actually

reveal to the end user.

And so that's another thing that we

might have to consider is what do we do to hide information

that we don't want to render.

But by default, if you have simple models like we do,

this is pretty productive.

OK, so let's stop the server.

We're going to go back over here.

And now we're going to add another one, app.get.

This is going to be band with a slug.

And then we want to nest songs on top of that.

So this is going to be very similar.

We're going to say async throws in.

And we're going to basically copy the loading the band

by slug code.

And because we're doing that, we've

already repeated this twice.

And if all of our routes look like this,

we probably want something a little bit nicer and reusable.

So what I'm going to do is go over to models and band.

And at the bottom here, I'm going

to create an extension on band.

And we can say static func find by slug.

And then we will pass in the slug as a string.

That'll be async throws.

Yeah, async throws.

And that will return an optional band.

Now, we could decide to have this throw the error

if the band doesn't exist.

I'm going to leave that up to the routing layer to do that.

But we can paste everything that we had before.

We're going to return try a weight band query on.

And that's where we need to pass in the database.

So we'll do that.

And now we have our first sort of query method

that we can use.

So I'm going to go back over to our routes.

And here we're going to say try a weight band.

And we will say find by slug.

Pass in the slug.

And then the database here will be request.database.

And we could probably combine the guard let here

to make that a little bit simpler.

And now we can copy that, put it here.

And now we have the same thing for both.

But this time we want to grab the band's songs.

So what we can do here is--

there's actually a number of things we could do.

We could say band.songs, which is a property.

So let's just try this and see what happens.

So we're going to go over here.

We're going to restart our server.

And then here I'm going to do /songs,

which should be that route.

So we actually got a crash.

And it says that children relation not eager loaded.

Use the dollar prefix to access.

So this is an important thing.

If you make a query like this, this query already

ran against the database.

And it only selected the attributes

for the band, which means that the songs are not loaded.

So we actually need to do something like songs.query

on request.database.

And then that needs to return all like that.

That needs to be try/await.

So let's restart our server.

And now we can see the song in an array for this band.

So that's one thing to note is that it

becomes pretty obvious when you're making another query.

So this is one query to find the band and then another query

to find all the songs for that band.

Now, that's not so bad.

What you want to avoid is--

let's say I was in a loop here and I said let songs equals.

And then I said for song in songs.

Then I want to load the artist.

And then I say song.dollarartist.query.

This is the type of thing that we want to avoid because then

we're doing what's called a select n plus 1,

which is there are n songs.

And we're doing n queries, one for each song

plus the one for this.

And so that's the type of thing that you want to avoid.

There are ways of joining the tables together

to return all the results together in a single query.

Sometimes that can be faster.

It really depends on your model.

But in many cases, it really doesn't matter.

Doing two queries instead of one is essentially free.

You just want to avoid the doing queries in a loop

or doing really expensive queries.

OK, so this gives us our songs.

OK, so now we have two routes, one for getting the band

and one for getting the songs for that band.

But these are sort of global application routes.

And what we really want to do is start organizing things

into controllers.

So I'm going to create a controller here called

bands controller.

OK, here we're going to import vapor.

And we're also going to import fluent

because we're going to be doing database queries here.

We're going to have a struct called bands controller.

And that's going to implement the route collection protocol.

So inside of bands controller, we're

going to add a func called boot.

And boot is going to take routes.

And that's going to be a route builder.

And then this can throw.

And this is where we're going to register our routes.

So we're going to say let bands equals routes.group band.

So this ends up creating a band group.

And the band group shows up in the path.

And then from there, we can say that we

want to do a get on another path parameter called slug.

So this is the same thing that we did before.

It's just now everything's in a group.

And then here, instead of passing

in a closure for the route, we're

going to pass in a function.

And I'm going to call the function show.

So we're going to call this show.

Now show has to match the type.

It's going to be a request.

This is going to be async throws.

And this is going to return a band.

This is a difference with the closure style

is that you have to specify what we're actually returning.

But I think this is useful because it

helps the compiler out.

And it gives you better error messages.

And it also sort of serves as documentation

if we're just looking to see what

this is supposed to work like.

OK, so now that we have this, let's go back into this one.

And we're going to just copy the routes there,

or the route logic there.

And we're going to say the same thing that we did before,

getting the band, returning the band.

And so now we have a bands controller.

So I'm going to go back over here.

We're going to comment this out.

And we'll comment this one out as well.

And instead of that, we're going to say app.register.

And we're going to pass in a collection.

And the collection is going to be our bands controller.

So this is how we can sort of tidy up our logic

and keep related things together instead of having one big

routes file.

OK, let's run a Swift build.

And this can fail, so we need to pass try there.

And one other thing we're noticing here

is that this is complaining about the fact that show

is a non-sendable function.

We can make this a sendable function

to silence that warning.

That's fine.

And the build is completed.

So now we should be able to get band retro rewind again,

and we do.

Let's also move the other function or the other route

for getting the songs.

And this is something that we may

want to have a songs controller.

Swift, it's important, vapor and fluent.

Songs controller, that will be a route collection.

We need a func boot, which takes a routes builder.

So we basically need this.

And then here we want the index for this,

which is going to come from the band.

So this is going to take our request,

return an array of songs.

This will be sendable.

For now, we'll just put an empty array there.

And now for the boot, we're going

to take the routes that we were given,

but these are going to be nested underneath bands.

So I'm going to say bands.register,

and we're going to register a collection underneath that.

And we're going to register it as a nested resource

underneath the slug.

So we can actually use a group there as well.

I don't know if we need anything other than this.

So we could say grouped with a path component of slug.

And then at this point, this would be the single band.

So we just say band.get.

This would say use show.

So getting the route just with band and slug in it does that.

And then we can also register a nested collection

for songs controller.

That can fail, so we'll mark it with try.

And then over here on this one, we're

going to do something similar.

So we're going to say routes.grouped,

and we can say songs.

Then we can say songs.get, and we're just

going to tell it to use the request, use index.

So now we go into our main routes file.

Let's copy this, and we're going to go into songs controller.

And we're going to uncomment that.

At this point now, we can say that we

have our slug parameter.

We get the band by the slug.

Otherwise, we abort.

We return bands.songs.query.

And this needs to be async throws.

And I think that's it.

Let's go ahead and give this a shot.

Now we do /songs, and we get the songs.

OK, so that is a look at routing and organizing things

into controllers.

In the next episode, we're going to talk about how to do things

other than just get, as in receiving content via post

and put requests.