Encode Type Information in Object IDs
![Drake Hotline Bling meme, he is disgusted by UUIDs and he likes IDs with types in them](/assets/types_in_ids_1280x720.webp)
Early in the creation of my blog, I decided to encode an object's type in its ID. My IDs are actually the type name joined with an alphanumeric nano ID of length 12. For example, Post_tsr8q6sx37pl
.
In this blog post, I want to share some of the advantages of encoding an object's type in its ID. I'll also show you how I generate these IDs using Prisma's schema language and Postgres. Let's start with the first advantage.
Get an Object's Type from Its ID
This is probably the most obvious advantage of encoding an object's type in its ID, and it is the reason why I chose to do it in the first place. Specifically, I chose to encode an object's type in its ID so I could easily implement the Node
interface of the Relay specification for GraphQL.
Because I have the type in the ID, I can create a function that parses the ID and returns the type. For example, in TypeScript:
type ObjectId = Nominal<string, "ObjectId">;
enum ObjectType { Post = "Post"}
type parse = (id: ObjectId): ObjectType;
Furthermore, I have this code in a package that is shared between the client and server. So I can reuse it on both ends.
This allows me to easily implement the Node
interface, among other things. For example, here is the implementation using TypeGraphQL:
@Resolver(() => Node)export class NodeResolver { @Query(() => GetNodeOutput) node( @Arg("id", () => GraphQLGlobalObjectId) id: ObjectId, @Ctx() context: GraphQlContext ) { const type = parse(id);
switch (type) { case ObjectType.Comment: return context.prisma.comment.findUniqueOrThrow({ where: { id } }); // ... all other cases default: throw new UnknownObjectTypeError("Unknown object type.", { extensions: { type } }); } }}
GraphQLGlobalObjectId
is a custom scalar that validates the object ID. If the value is not a valid object ID, then the client will get a GraphQL error.
This is all technical, but there is less technical advantage to encoding an object's type in its ID.
What the Hell am I Looking At?
Another advantage of putting the type in the object ID is that I can know immediately what table to query when I see the ID in logs or in error meta data.
Do you frequently see UUIDs in error logs and think, What the hell am I looking at? If the type is in the ID, then you don't have to guess.
Next, I'll show you how I generate these IDs using Prisma and Postgres.
How I generate Object IDs with Prisma
Finally, here is how I generate object IDs using Prisma's schema language and Postgres. First, here is an example of my Prisma schema:
enum ObjectType { Post}
model Post { id String @id @default(dbgenerated("global_object_id('Post'::\"ObjectType\")"))}
There is an unfortunate issue with Prisma's type generation, causing it to not generate enums unless they are explicitly used in a model.
In addition, I have a migration that adds the global_object_id
function to Postgres.
CREATE TYPE "ObjectType" AS ENUM ('Post');
DROP FUNCTION IF EXISTS global_object_id("ObjectType");CREATE OR REPLACE FUNCTION global_object_id(objectType "ObjectType") RETURNS text LANGUAGE plpgsql VOLATILE PARALLEL SAFEAS$$BEGIN IF objectType IS NULL THEN RAISE EXCEPTION 'Object type is required.'; END IF;
RETURN concat(objectType, '_', nanoid(12, '0123456789abcdefghijklmnopqrstuvwxyz'));END$$;
And the nanoid
function was taken from here.
Any Drawbacks?
So far, I have only encountered two (solvable) issues with this design.
The first issue is that I decided to use mixed-casing for my type names. The problem with that is URL paths are case-sensitive. Occasionally, someone will try to view a blog post with a URL that is lowercase, which results in a 404. I could fix this by treating these URLs as case-insensitive on the BE, though.
The other issue I encounter is on the Prisma migration side of things. When I add a new type, the ObjectType
enum in Postgres needs to be updated and committed before I can reference the new type. For example:
ALTER TYPE "ObjectType" ADD VALUE 'Comment';COMMIT;
Prisma's migration tool does not commit the enum update before continuing with the migration. This causes the migration to fail, and then I have to go through the painful process of resolving a failed Prisma migration.
Shout out to this blog post for showing how to make Prisma ignore a migration change. Luckily, this issue surfaces itself during development, so it gets fixed before going to production.
One more drawback of this design is that it has reduced the portability of my database schema. For example, if I want to move off of Postgres for whatever reason, then I have made that more difficult. However, I'm already using a bunch of Postgres features that aren't available in something like SQLite, so it would be a lot of work anyway 🤷♂️.
Conclusion
I'm really happy so far with the decision to encode an object's type in its ID. It seems to provide a lot of value without really having any drawbacks.
In a future post, I may talk about another design decision I made where having an object's type in its ID is really useful.
What do you think? Do you like the idea of encoding type information in an object's ID?
Written by Daniel Nagy
Daniel is a software engineer and full-stack web developer. He studied computer science at Ohio University and has been doing web development and hybrid mobile app development since 2014.
If you liked this post, then please consider donating or becoming a sponsor. Your support will help me produce more content that gives back to the community.
Comments
You will not receive notifications for new comments. If you are waiting for a reply, please check back periodically.