Client-side resource IDs.

Solving many problems with little to no drawbacks.

Usually, when you create a resource, the API will generate an ID for it before saving it. Or even database might do it for you. There is a different way, and you might like it.

Resource IDs

Let's make sure we're on the same page when it comes to resource IDs. This would be the unique ID that is assigned to an entity (a user, a blogpost, a product, etc.). It selects, deletes or updates particular objects.

Server-side IDs

Most commonly used are server-side IDs. These are the simplest to implement, most of the time requiring little to no input from the developer. It might be the database that can do it for you (like MongoDB adding _id fields) or you're generating some sort of UUID.

Whenever you wish to create, let's say, a new User, you'd send something more or less like this.

{
	"email": "hello@example.com",
	"fullName": "Tomasz Gałkowski"
}

You'd usually get something like that in response.

{
	"id": "some-randomized-id",
	"email": "hello@example.com",
	"fullName": "Tomasz Gałkowski"
}

Client-side IDs

Client-side IDs are created on the client, obviously. When talking web — that's usually a web application or mobile app. On the client, we can't leverage database doing it for us. But we still have the possibility to generate random strings like UUIDs ourselves.

This time, when creating a new User, we'll generate the UUID on client-side and send it with the payload.

{
	"id": "some-randomized-id",
	"email": "hello@example.com",
	"fullName": "Tomasz Gałkowski"
}

And we'll get the same response.

{
	"id": "some-randomized-id",
	"email": "hello@example.com",
	"fullName": "Tomasz Gałkowski"
}

Why would I ever bother?

Why would you ever bother? That's a good question. Client-side resource IDs have some advantages over the "good old ones".

Decoupling

They force your backend developers to decouple their business logic from their infrastructure. By not relying on the behaviour of the database, it creates some separation. Business needs or performance might force you to change the database, and you'd be screwed.

Types are neater

This one is true in TypeScript and Go, but I'm certain this applies elsewhere too.


If we take a look at the User example from the previous paragraphs. Our User could look like this.

type User = {
	id: string;
	email: string;
	fullName: string;
	role: "admin" | "user";
}

What if for some reason (whatever it might be) we need to create that user before we assigned it an id? Should we go ahead and go with id?: string? That feels wrong.

Or maybe we should create a separate command type.

type CreateUserCommand = {
	email: string;
	fullName: string;
};

async function createUser(cmd: CreateUserCommand): Promise<User> {
	const user: User = {
		...cmd,
		id: generateID(),
		role: "user".
	};

	await aServiceThatSavesData(user);
	return user;	// 1
}

function findOneUser(uid: string): Promise<User | null> {}


That's certainly a way to do it.

Notice // 1. We need to somehow return the result of creating the user because we have no other way to communicate the ID. How would our client delete this resource now without listing all of them?

But our commands shouldn't really return data. Queries should do that. What if we go with this instead?

type User = {
	id: string;
	email: string;
	fullName: string;
	role: "admin" | "user";
}

type CreateUserCommand = {
	id: string;
	email: string;
	fullName: string;
}

function createUser(cmd: CreateUserCommand): void {
	const user: User = {
		...cmd,
		role: "user".
	};

	await aServiceThatSavesData(user);
}

function findOneUser(uid: string): Promise<User | null> {}


Client would have to generate the ID and pass it alongside any other required payload. We don't have to return anything to a user outside 201 Created because they already have the ID handy.

Handling errors

You might be asking — OK, but what about ID collisions? Or any other error for that matter?

When using UUIDv4 or similar algorithm, collisions wouldn't be a problem. They are rare enough to handle them as an error. Server-side resource IDs aren't free of that issue, but they could regenerate the ID when an error happens. On client — we'd have to rerun the request.

Idempotency

Outside from cleaning backend types and making it slightly easier to separate concerns. There is one more thing that is very useful.

Let's imagine you're making a POST request. Let's say — add a comment. POST requests have permanent side effects, and the result will be different if you send them once or many times. You might add one comment or five comments.

Client-side resource IDs can work as a poor man's idempotency tokens. If your app had some connection issues and your user sent add comment ten times, you only want to save a single one. Depending on how (and when) the IDs are generated — as long as the front-end sends 10 identical requests — only the first one will go through. Subsequent ones will fail due to unique constraints.

Offline-first

Client-generated IDs have one more pro — they don't require a server. You can do countless things on the client now. You could easily move a big part of your logic to the client device and fallback to being offline when the server is down. Not only that, but you don't have to wait for a connection to create new resources.

It can be done with server-generated IDs, of course! It might be just that tiny bit easier to do it on the client, though.

Cons

OK, but what are the downsides? Not that much, frankly. Most of the problems that you could come up with would be possible with server-side generated IDs, or they are not really a problem.

Sending non-unique IDs?

There is a chance the client will send non-unique IDs. The chance of it when using something like UUIDv4 is small, however — it exists. What would happen? Ultimately, the server would try to save them to the database and fail. The client would receive a message about failure, regenerate the ID and try again. No biggie.


However, a server-side application has to properly utilise transactions to roll back any actions it did before it failed to insert or check for key uniqueness before starting work. That could be costly in performance.

My best bet would be to manually check uniqueness before some heavy operations and letting it fail (with proper error handling!) on the simpler cases.

Security?

One might be concerned about allowing clients to pick their resource IDs. True. But in the simple example above, I could inject anything in the email as well as the id. Nothing excuses validating and sanitising data, in Node.js you might want to use joi or class-validator to make sure the ID is in a correct format. In Go, you might take a look at go-playground/validator.

Summary

There are surely some more sophisticated solutions, but I've grown to like generating IDs on the client. It makes some things easier, while drawbacks to be outweighed by the pros.

What do you think?