Struggles with Vapor 3
I am working on my very first “real” side project since 2006! Not counting a bunch of open source projects I started and maintained since 2006, no, I mean a real user-facing project with a backend, website, iOS app, the whole shebang. It's been a while actually since I wrote a whole backend from scratch and where I used to default to Python for this, since 2016 I've spent 95% of my time doing Swift. Now that there are multiple “Swift on the server” frameworks available, I decided to stick with Swift for my backend, went with Vapor 3, and dug in.
My first impression was really positive: it's super easy to quickly build out a bunch of models since they're just normal classes with normal String and Int (and so on) properties. The router, controllers, and middleware were easy enough to get started with as well, and before I knew it I had my first endpoints ready to go.
And sadly, as I started to add more endpoints and more features, I kept running into issues and felt everything was a bit of a struggle with the framework.
The first thing that I ran into: what if you want to use different data in the POST versus the GET? This is super common. For example, when creating a new object you don't supply the userID in the POST since it should automatically be added to the currently logged in user. Yet in the GET, you do want to give that user object back. Vapor's solution is to use different structs, different representations of your internal model, to model the incoming and outgoing data. The positive thing about that is that everything is strongly typed, it's super clear what is going on with your data. The big downside: you keep having to translate from one model to another, and you need to keep at least two (but often three) models in sync. So when you add a property to your internal model, you also need to add it to your public model for example and change the code that translates one model to the other. This gets pretty tedious pretty fast.
Still, that's not the big issue. I wouldn't call it a dealbreaker — it's just a bit annoying but you gain all the security by having these strongly typed models, the compiler helps you with refactors; all the upsides of having a strongly typed language.
Relationships in Vapor models, however… that is a different story. You see, relationships are not automatically fetched when you want to return a model, so they are not part of the returned JSON. For example, your BlogPost object has a relationship to a User object (the owner): you'll get the userID back, but not the User object. Or if your Album object has multiple Track objects, you won't get anything back at all. The solution is again to use a different public data version of your model, fetch all the relationships yourself, and then fill that public model with the data and return that. It's honestly a lot of boilerplate work to keep doing and all the async behavior or Vapor doesn't make it any easier either.
(Maybe the “proper REST way” is to add different endpoints for all (nested) relationships, but that really sucks for consumers of the API, and not something I want to do to myself.)
When you want to store extra information with your many-to-many relationship (called a “pivot” in Vapor), you end up doing a lot of queries yourself as well. For example, let's say we have Students and Courses, and in the relationship we want to store the student's progress in the course. When you fetch
course.students, none of the extra fields are there, you'll always have to manually fetch from the pivot table, filter on the course and/or student you're interested in and then do something with that. It might not sound like a big issue but I kept running into all these things that make working with relationships in Vapor just a bit too annoying.
Some other database and model related pain points I ran into:
- Model migrations are a pain to write, especially compared to Dango's magical
- Change database? Change imports, models (
PostgreSQLModel) & migrations (
PostgreSQLConnection), all over the place. Very strange that your entire codebase needs to know about the specific database like that.
- It's cumbersome to use String enums in your models, whereas Int enums work just fine out of the box.
- Specifying things like unique fields or indexes in your models is more verbose and boilerplate-y than I'd like.
Most of the problems I have with Vapor 3 are database- and model related, and I have heard that this will all be changed in Vapor 4, due for release “sometime this year”. The proposals for the new models indeed look to solve most, if not all, of my problems. However, it'll also be backward incompatible so that makes me wary to keep working with Vapor 3, as I am still such an early stage in this project. Do I wait a bit for Vapor 4 and simply not work on my side project? No, that is ridiculous. Do I stick with Vapor 3 despite the problems? Or do I just use Python (for example with Django and Django REST Framework)? That is the big question I am now trying to answer.
Some things that are also playing a role:
- Documentation for Vapor is pretty sparse, there is very little information on Stack Overflow or in tutorials around the web. There is a ton of information for Django and Django REST Framework, which does make a difference when you run into problems or questions. The Vapor Discord is helpful and friendly though, which should definitely be mentioned.
- TokenAuthenticatable: the token is not encrypted in the database at all, so when you have access to the database and thus the tokens, it's the same as having access to the accounts. This really should be encrypted. Django REST Framework by default actually has the same problem, but by installing django-rest-knox, that's easily solved.
- There are not a lot of packages available. For example, something like an automatically generated admin site (like Django has) would be great for MVPs. It's not really an issue for my side project, but it would hold me back from using Vapor for actual paid projects since I'd have to write so much stuff from scratch. Most projects simply don't have the time and budget to write an admin interface from scratch, sadly. Of course, Vapor is a pretty young project and I'm sure it'll get better soon! In fact, I hope I'll be part of the growing ecosystem going forward.
I am really looking forward to the model changes in Vapor 4, but until that time I am between a rock and a hard place. Stick with Vapor 3 and its pain points, or lose the strongly typed safety and help from the compiler by switching to Python. I just don't know…