Episode #578

Creating Songs with a Custom Payload Struct

Series: Build a Vapor Backend

18 minutes
Published on May 29, 2024

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

In this episode, we create a form to create songs from our API by implementing a create route. We use a payload struct to normalize and validate user input for song title and artist name. We also ensure that duplicate artists are not created by using a custom comparison method. Finally, we create the artist and song in the database and return the newly created song. We also configure the JSON output to use snake case instead of camelcase for our default encoder and decoder for our Vapor app.

This episode uses Vapor 4.92.

OK, in this episode, we're going to take a look at how to create a form to create songs from our API.

So I'm sitting here in songs controller, and I'm going to create another function here.

It'll be a sendable function called create.

It'll take the vapor request, it'll be async throws, and then it will return the song that got created.

So what I want to do here is say that songs.post is going to use that request, that new function that we created.

And create is going to take the slug in order to load the band.

So we'll have that same behavior here.

And then we're going to end up creating some sort of form that will look like this.

Let's sketch this out for a second.

So we're going to post something that looks like a song title.

And that song title-- I guess this needs to be in quotes too.

Song title would be something like this.

And then artist name would be something like that.

And what I would like to do is find or create this artist, and then create the song and associate it with the artist.

And that will all be scoped underneath the band.

The band itself will come from the slug that we posted to.

And so that will allow us to do everything that we need.

I don't want to require the user to post the actual song model that we store in our database, because there are things that the client isn't going to know about-- the internal structure, et cetera.

So what I'm going to do here is I'm going to create a struct called a createSongPayload.

And that's going to be a content.

Content is a layer on top of Codable.

And so what we can do here is say that we've got a song title, which is a string, and we've got an artist name, which is a string.

OK, so now that we have this createSongPayload, we can grab that from the request.

So we can say, on our request, we can look at the content in the request.

This is going to be the body content.

And then tell it we want to decode the createSongPayload.self.

And I'm going to call that payload.

That can fail.

So we use try here.

And then now that we have this payload, we can then find or create the artist.

So let's first look at the band.dollarartists.

And we're going to say query on request.db.

That's REQ.

So this is going to already scope our query to only include the band's artists.

And we're doing this because we want the artist to be entirely housed underneath the band.

We don't want to reveal to anyone else the artist that other bands may have created.

It's OK to have duplicates, but not OK to have duplicates underneath the same band.

So this gives us all the artists.

But we then want to filter those where the $name property equals the payload.artist name.

And then we're going to get the first one.

That's going to give us an artist like this, which could be optional.

So I'm going to say if let artist-- we're going to use request.logger.info.

And this is how we can log things in our application.

So here I'm going to say found artist.

And I'll say artist.id.

Otherwise, we're going to say new artist creating like that.

OK, so this is also try/await because we're doing a database query.

And I think this is fine.

I'm just going to force unwrap that for now.

So if we have an artist, we're going to print this out.

Otherwise, we're going to print that out.

And so this will tell us if we found that in our database.

So I'm going to go ahead and we have our database running.

I'm going to create a new tab here.

And I'm going to say Swift run app.

And then I'm going to open up a new split here so that we can run curl dash x post.

Looks like I have an error here.

This is missing return in songs.

Yep.

So here I'm actually going to just throw an abort and use I am a teapot just as an example just so that we can continue on without actually having to create the song until we're ready.

So over here, we're going to run the application.

And then I'm going to post.

I'm going to post some data here.

And we're going to have it look like this.

This is going to be-- let's do it like this.

Song title is-- and this time, we're going to say nothing but a good time.

And the artist name is going to be poison.

OK.

Once we've done that, we grab HTTP, local host 8080.

This is going to be band retro rewind and songs.

And because we're posting it, it's going to know that we're doing that create action.

Let's see.

That needs to be dash x capital.

OK.

So we got our error.

Value of type string was not found at path song type.

So you notice that I'm using this style of keys for our API, which is something that I am just used to.

And that's how I like my APIs to look.

But Swift naming is different.

So if we go back over here, we see that we've got song title and artist name.

Now, we could, if we want to, just do this.

But we can also configure the JSON decoder so that we can have our own song title.

The JSON decoder so that we can have our entire application work the way we want.

So I'm going to go over here to the configure function.

And I'm going to make a private func configure database.

And it's basically just going to let us tuck a bunch of this logic in here and keep this method clean.

So we're going to say try await configure database, like that.

And the database is configured.

And now I want to create my decoder.

This is going to be a JSON decoder.

We're going to use a key decoding strategy is convert from snake case.

We're going to use a date decoding strategy, which will be ISO 8601.

Then we're going to create an encoder, JSON encoder.

The encoder's key encoding strategy is going to be convert to snake case.

And the encoder's date encoding strategy is going to be ISO 8601.

Now, also on the encoder, we can say output formatting equals pretty printed, just so that over here when we get these messages here, we can have them look a little bit nicer in the terminal.

This is something you probably wouldn't want to do in production, because it's going to add unnecessary white space.

But it's nice during development.

So we've got our decoder and encoder.

And what we're going to use is content configuration.

And here we can say global.

And then I can say that I want to use this decoder.

This decoder.

And then for the HTTP media type, this is going to be for .json.

Then we're going to say content configuration.global.use encoder for .json.

So this means that this is going to be the global for the entire application.

You can also decide to use just for certain endpoints if you want to, if you had some sort of specific need.

But this is going to work for our use case.

So I'm going to go back over here.

We're going to kill the server and rerun it.

And then I'm going to repeat this request.

And we get the same error.

And the reason for this is that it doesn't actually know that our content is JSON.

And so we're going to have to tell it by giving this a header.

So I'm going to go over here and say dash capital H.

And we're going to say content type is application slash JSON.

And now we get the actual error, I'm a teapot, which means we got through the entire thing.

But we also see that we found this particular artist.

And that's because this artist already existed in the database.

So we're doing pretty good there.

And if we take a look at the database portion of this, we can see the select artists where the artist.bandID is $1 and the artist name is $2.

So that's all working.

What we want to do now is protect ourselves against a couple of issues.

So if we take a look at our create song payload, we want to avoid anyone sending complete white space or empty values.

And so we're going to be trimming the values, removing any kind of trailing or leading spaces, new line characters, et cetera.

And we're also going to validate that these are not blank.

So what I'm going to do here is every content object has an after decode method.

And there's also a before encode if you want to do something specific there as well.

And this actually throws, so this gives us an opportunity to guard against invalid input.

So here we can say, I'm going to let the artist name equals the artist name here, trimming characters in white spaces and new lines.

And then we can guard that artist name is not empty.

And if it is, then we can throw abort.

And here we can say bad request.

And we can give it a reason saying artist name is required.

Then we can set artist name equals to that artist name.

And we're going to do the exact same thing here, except this is going to be song title.

Song title, the song title is required.

And then the song title is equal to the song title.

OK, this needs to be a mutating func because we're mutating a struct.

And these both also have to be vars for that to work.

But that's fine.

So this will grab what the user has submitted.

And then we will normalize it here and reject empty values.

So let's give that a quick test and go back over here.

We're going to rerun the server.

And then over here, we're going to return.

I'm going to do poison with some spaces at the end.

And we can see that we also found the artist there, which is great.

And if I delete all the characters and just include spaces, we can see that we got an HTTP 400.

The artist name is required.

OK, so that is one issue that we can guard against, this invalid input.

The other is if somebody had typed in a artist name that differs only in case.

And so when we try to find it, we're not going to find it if we're matching case exactly.

Now, there's a couple of different ways to do this.

But instead of using equal equals, we can use a custom I like here to use as the comparison.

And this is a Postgres feature.

If you're using MySQL, you'd have to do something different here.

But this is a way for you to do a case and sensitive comparison between this property and this value over here.

So let's give that a quick test.

We're going to run this.

We're going to go back over here, and I'm going to add poison.

But this time, I'm going to do it just all lowercase.

And it should still find the artist, and it does.

So that's going to prevent us having multiple records that only differ in case.

OK, so our artist here, we've got this first artist.

If we don't have the artist, then we're going to want to fall back.

I'm going to delete this because we don't need to do that.

We can fall back to the-- let's see, try await artist dot create.

Let's see, how do we want to do this?

We can create the artist with the name and the band ID.

So we're going to say payload dot artist name.

And the band ID here is going to be band dot ID.

And we can use a require ID.

And let's see, this needs to be try here, not here.

OK, so we don't really know if we've created the artist yet.

So I'm actually going to say artist, existing artist like that.

We're going to do this in two steps.

And then I'm going to say let artist is an artist.

If there's an existing artist, then we're going to set artist equals the existing artist.

Otherwise, we're going to create one.

And then here I need to call save on request dot DB.

That is a try await.

So now we have our artist created.

The next thing to do is to create the song.

And this is going to be a song.

And the song takes title, artist ID, and band ID.

So we will do payload dot song title.

This will be artist dot require ID, which is try.

And then this will also be try band dot require ID.

OK, now we can try await song dot save on request dot DB.

And then we can return the song from our API indicating that it was successful.

OK, so we're going to try this again.

This time it should actually save.

And here we get the song that included the artist, the artist ID, created that band, band ID.

And you can see here that this is something that we can control what we actually return.

Instead of returning song, we might want to return a create song response or something like that.

But this is giving us everything that we need to indicate that it was successful.

And critically, it returns the IDs, which the client may use, in order to make further queries against this record.

And with that, we can now create songs from our API. (upbeat music)