A good model for REST resources?

I am writing some model code for exchanging information using a REST API. The usual sample code is quite simple:

struct User: Codable {
    let id: Int
    let name: String
    let password: String
}

And then I can encode/decode this to/from JSON. But once we get from a read-only API to a richer one, things start to break down quickly. For example, the password field may only be supplied for POST, backend may never return it. And the id may obviously never be changed by the client. Generally, the set of constraints for the properties may vary wildly across methods (POST, GET, PATCH, …) and the direction (request/response).

Writing in a language with a strong static type system, I would naturally like to express as many of these constraints in types as possible. But currently the only solution I can think of is creating multiple versions of the User type, which leads to code duplication, explodes the number of visible symbols and generally feels stupid.

Are there tricks to solve this nicely? How do people go about this?

Offhand the first thing that comes to mind is something similar to multiple Users objects, but with a little less duplication

struct UserPostView : Codable {
  let user: User
  
  func encode(to encoder: Encoder) throws {
    // Encode the fields that matter here, using User.CodingKeys or something similar
  }

 // Same with Decodable
}

struct UserGetView : Codable {
  let user: User
  
  func encode(to encoder: Encoder) throws {
    // Encode the fields that matter here, using User.CodingKeys or something similar
  }

 // Same with Decodable
}

This approach saves you from duplicating the User and all its functionality. One main issue there is that the fields that might not be sent would need to be optional, or decode into a sentinel value.

1 Like

It does actually seem like you are overloading the meaning of User. For example, it seems like the request which requires a username & password (but probably not the user-id) could likely also be called a LoginForm, and the response (not including password) could maybe be called a UserProfile or something.

2 Likes

Coming up with good examples is my weak point :) How about a payment transaction? You post a basic info about the transaction, get a transaction with extra fields back and some write-only fields missing? I understand that I could try and come up with some reasonable names for all the versions of the object to justify their separate identity, but that usually just ends up as UserRequest and UserResponse – and that feels too primitive.

This is actually a very good question, as it's something that many beginners struggle with, and is rarely explained.

Unfortunately, there's no straight-forward answer to this, but as Karl said, you should focus on not overloading types. To give you an example, here's how I (currently) use Codable for a type like User, in a server-side application that has both REST endpoints as well as server-side (rendered) pages.

  1. There is a main type User. This type does not care about what is sent to/from a user. It models a User the way it should be modelled internally.
  2. User is Codable. Its codable form is the representation I store in the database.
  3. For every REST endpoint, there is a Codable type that models the response. Even if this response is or contains a User, it is a separate type.
  4. For every server-side page, there is a Codable type that models the rendering context for this page. Again, even if I'm rendering a User, there is a separate type for each page's context.

Yes, this involves a lot of similar types but I don't consider this code duplication as these types are fundamentally different. Keeping these types separate actually makes your code easier to write, understand and maintain as it greatly improves local reasoning. For example, if you want to change the response of a REST endpoint that returns a User, you can just change the type for that response, with no consequences for the rest of your app. If there was only one type User that was used in different ways throughout the app, things would not be as easy...

As a general advice, I recommend reading up on (enterprise) design patterns. There are lots of useful ideas in there to help you answer design questions like this.

3 Likes

On the other hand, changing a single property type on the conceptual User type means going through a multitude of implementation types? But I guess that’s the usual tension between code reuse at the cost of complexity and code duplication with the benefit of simplicity.

(I was hoping for a pattern that would allow me to model just the “main” type and then describe the different constraints for different HTTP methods somehow. The views suggested by Erik are an interesting angle I will think about.)

Would you have a more particular pointer? One could spend several lifetimes googling “enterprise design patterns”, and most of those lifetimes would be quite painful. Thank you!

Actually, no. You should view these as independent. It is up to every type to decide what properties it has. Sure, they will be related if they all represent a user somehow, but you shouldn't view them as the same property.

Some examples:

  • User has a property id of type ObjectId (from MongoDB). In related types, this property is either a String, or not included at all.
  • Location has a property distance of type Double. In related types, this property is an Int (rounded).
  • I use Date in my model types. In related types, these get converted to either Double (timestamp), String (ISO) or String (localized).

Obviously the classic Gang of Four book. A bit dated, but still a required read. For enterprise patterns, a good reference is "Patterns of Enterprise Application Architecture" by Martin Fowler.

2 Likes

I find that very few requests require the client to send the same data back to the server as it receives as a response. It depends on the API you have, but in my experience typically the client:

  • has part of the data (e.g username & password) and wants other data (e.g. user profile) back from the server, or
  • wants to perform an action on some known remote entity (typically requiring an ID, plus any action-specific parameters)

So I don't think it's a good idea to send a User object to perform a login operation. There may be some overlap, but really they are used in entirely different contexts.

The code-reuse happens because both the client and server can use the same response type (e.g. struct UserProfile). The server can construct and populate an instance however it wishes, serialise it to some wire format, and the client can use the exact same definition when deserialising. If you do need to send entire objects back (e.g. forms), then this process also works in reverse, of course.

I completely agree with everything @svanimpe and others have written; I'll just additionally highlight that you'll probably want to have as much input checking, sanitising etc. as possible at the boundaries of your application. If you do that, within the rest of your application you'll never have to worry about "bad" / invalid / "malicious" data (or even if you do, for additional safeguarding, you can just fatalError or use the type system instead of having to deal with proper error handling).

This isn't always 100% possible. If you do something very complex with user input it might be that you only know there is a problem after a lot of internal processing and you'll have to work with explicit error handling. Another case is if you have something like a database integrity violation (e.g. a user submitted a form twice and due to race conditions only the DB can catch that the email actually already exists).