Opinion: Don’t mutate the input or output of your services (most of the time)

Sean Nicholas
4 min readJul 31, 2018

--

Okay, here’s a quick overview so you know what to expect: First I will tell you what I mean by input & output mutations, then I talk a bit about why I don’t think they are a good idea and then present you how I would do it instead. At the end I explain where I break my own concept. 😉

Input & Output Mutations

Every feathers service can have an arbitrary amount of before and after hooks. They run, as their names suggest, before or after the service has done its job. A hook has access to the user’s input (data & query) and output of the service (data send to the user). It can perform all kinds of stuff. I.e. send mails, validate data or change the input / output… Wow, stop. ✋ The last one — that should not be done.

Why mutation might not be a good idea

TL;DR: Mutation reduces transparency on the client and transfers client logic to the server.

There are three parts I want to go into detail. User Data, Queries and Service Output.

Let’s start with the most interesting one as I think it is mostly not clear why you should not change queries. Imagine the following scenario: A user should be able to get his messages from the server but he should not see messages from other users. The easiest way might be to use the queryWithCurrentUser hook. It changes the query and adds the user id. Seems pretty nice. But now your boss comes to you and says the support team needs access to all messages from within their region for customer support. Now you need to extend your hook. And as you work it, why not add that admins can access all messages? So you have a slightly more complex hook:

  • Normal user’s: only own messages
  • Support: query with their region id
  • Admins: all messages

Now let’s look at the client. The code for all these requests looks exactly the same:

app.service(’messages’).find()

Sooo… what’s the problem?

First, it is hard to understand what the code does. You need to know the server internals if you want to understand the client.

Second, now the admins see all messages in their inbox because there is no query restriction for them anymore.

Same with user input data. If you mutate it, it makes thinks like admin tasks more complex on the server. I.e. if you are adding the userId to every message in a before hook, you will have a hard time if admins should be able to add a message in the name of another user.

What about the output of a service? Mutating the output makes developing the client much harder because you introduce state that is not transparent to the frontend developer. I.e. only return certain fields if user is admin or not. (For another example read the text at the end of this post)

A more transparent solution

So, how to solve these problems? Well, clients should always need to provide the correct data for the service otherwise their request should fail.

Let’s look at queries to make this more clear. The client’s code for the inbox should look something like this:

app.service(’messages’).find({query: { userId: currentUserId }})

The server should only assert that the query contains the userId. If it does not and the user is not an admin, the whole request should fail. (Side note: This is how firebase Firestore works where I cribbed the concept 😉)

Same for data input. Just validate it. If the user should only be able to add messages for himself than enforce it, but don’t secretly add his userId.

And last, the service output. Try to design your database so that users can access the whole document. It makes the thought process much easier. But of course sometimes you need to filter out a password or something. Do it. But keep it to a minimum.

Where I break my own rules

Of course, I remove passwords from the users doc. This is a no-brainer. But there are times when I need to augment the service output with more data. I always use a _ at the beginning of those fields to mark them as server generated. I also remove all fields that start with a underscore from the user input so that there is no chance to save a doc with _ keys.

Did you spot it? I broke my rule again. I mutate the user input by removing _ fields. Well, there is a reason. I often get a doc from the server, mutate it on the client and send it back as a whole. So there might be underscore fields in the update request. Therefore I remove them at the server. I might change that in the future and throw an error. But currently I am to lazy 😁.

That’s it. That’s how I design my apis to make things transparent and easy. Don’t forget it is only an opinion and I just use this concept since a couple of months. If you have improvement suggestions or think differently feel free to share 😊

Oh, one more thing… There was another example for service output mutation I wanted to share with you. Want to know where I didn’t follow my rules and it bit me badly? I wrote a content service. It could recursively get other content and replace a placeholder with it. For example:

Contact

E-Mail: [content:email]

Would be resolved to:

Contact

E-Mail: foo@bar.com

I mutated doc.text directly. First it did work nicely. I created a bunch of content docs. But then I wanted to edit some of them and realized: I get the replaced content back. There is no way to get the unreplaced data with the placeholders anymore. 😱 Well, I think I need to rebuild the hook. This time with an _ field 😉

--

--