Skip to content

How useful is stripes prefix id approach in api models

Yes, Stripe’s prefixed ID approach (like cus_ for customers, sub_ for subscriptions, pi_ for payment intents) significantly reduces developer errors in several ways:

Type Safety at Runtime

  • Immediate visual confirmation that you’re working with the right resource type
  • Prevents accidentally passing a customer ID where a subscription ID is expected
  • Makes debugging much easier when IDs are logged or displayed

Self-Documenting Code

// Clear what each parameter expects
function processPayment(customerId: string, paymentIntentId: string) {
// Easy to spot if someone passes pi_xxx as customerId
}

Fail-Fast Behavior

  • APIs can validate the prefix before processing, giving clearer error messages
  • Catches mistakes early rather than deep in business logic
  • Reduces support tickets from confused integration attempts

Database Benefits

  • Helps with data integrity checks
  • Makes it obvious when examining raw data what each ID references
  • Useful for database constraints and validation rules

GraphQL/REST API Design In your domain-driven architecture, you could adopt this pattern:

type CustomerId = `cus_${string}`
type SubscriptionId = `sub_${string}`
// TypeScript can enforce these at compile time with template literal types

The main tradeoff is slightly longer IDs, but the error reduction and developer experience improvement far outweigh this cost. It’s become an industry best practice that many other APIs have adopted (Shopify, Twilio, etc.) for good reason.


Shopify Examples

Shopify uses similar prefixed IDs across their API:

// Shopify ID prefixes
'gid://shopify/Product/123456789' // GraphQL Global ID format
'prod_abc123' // REST API format
'var_xyz789' // Product variants
'ord_def456' // Orders
'cust_ghi123' // Customers
'coll_jkl456' // Collections

Framework Support

OpenAPI/TypeScript

Zod + OpenAPI Generator supports this well:

import { z } from 'zod'
const CustomerIdSchema = z.string().regex(/^cus_[a-zA-Z0-9_]+$/)
const SubscriptionIdSchema = z.string().regex(/^sub_[a-zA-Z0-9_]+$/)
// In your OpenAPI spec
const schemas = {
CustomerId: {
type: 'string',
pattern: '^cus_[a-zA-Z0-9_]+$',
example: 'cus_abc123'
}
}

tRPC (TypeScript RPC framework):

import { z } from 'zod'
import { router, publicProcedure } from './trpc'
const customerId = z.string().brand('CustomerId')
const subscriptionId = z.string().brand('SubscriptionId')
export const appRouter = router({
getCustomer: publicProcedure.input(z.object({ id: customerId })).query(({ input }) => {
// TypeScript knows input.id is branded as CustomerId
})
})

Go Server Frameworks

Go with custom types:

type CustomerID string
type SubscriptionID string
func (c CustomerID) Validate() error {
if !strings.HasPrefix(string(c), "cus_") {
return errors.New("invalid customer ID format")
}
return nil
}
// With Gin framework
func getCustomer(c *gin.Context) {
var req struct {
CustomerID CustomerID `json:"customer_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if err := req.CustomerID.Validate(); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
}

Fiber framework with validation:

import "github.com/go-playground/validator/v10"
type CreatePaymentRequest struct {
CustomerID CustomerID `json:"customer_id" validate:"required,customer_id"`
Amount int `json:"amount" validate:"required,min=1"`
}
// Custom validator
func customerIDValidator(fl validator.FieldLevel) bool {
return strings.HasPrefix(fl.Field().String(), "cus_")
}
func init() {
validate := validator.New()
validate.RegisterValidation("customer_id", customerIDValidator)
}

TypeScript Branded Types + OpenAPI

For your CDK/AWS setup, you could use:

types.ts
export type CustomerId = string & { readonly brand: unique symbol }
export type SubscriptionId = string & { readonly brand: unique symbol }
export const createCustomerId = (id: string): CustomerId => {
if (!id.startsWith('cus_')) {
throw new Error('Invalid customer ID format')
}
return id as CustomerId
}
// In OpenAPI spec generation
export const CustomerIdSchema = {
type: 'string' as const,
pattern: '^cus_[a-zA-Z0-9_]+$',
description: 'Customer ID with cus_ prefix'
}

Effect (functional TypeScript framework) has excellent branded type support:

import { Schema } from '@effect/schema'
const CustomerId = Schema.string.pipe(Schema.pattern(/^cus_[a-zA-Z0-9_]+$/), Schema.brand('CustomerId'))
type CustomerId = Schema.Schema.Type<typeof CustomerId>

The pattern works best when you have strong typing systems that can enforce the prefixes at both compile-time and runtime, which your TypeScript + domain-driven approach would handle well.


Based on my research, here’s a comparison of Effect vs TypeBox in terms of ease of use and capabilities:

Learning Curve Comparison

Effect: Has a steeper learning curve - “You need to learn how to use the library - it’s not as simple as writing plain TypeScript code” and “The code is more complex than the one without Effect; it may be hard to understand for people who are not familiar with the library”. However, “its benefits far outweigh the initial learning curve”.

TypeBox: Much easier to learn. TypeBox has a simpler syntax that “looks like JSON Schema with TypeScript flavor” and is described as “fast and type-safe way to validate data in TypeScript”. The API is straightforward and familiar to TypeScript developers.

Capabilities & Use Cases

Effect is a comprehensive functional programming library that:

  • Aims to replace multiple libraries: “potentially replace specialized libraries like Lodash, Zod, Immer, or RxJS”
  • Provides advanced error handling, async operations, dependency injection
  • Has “really nice aspects like the interoperability between Schema and Data, allowing you to safely parse data from outside your application boundary”
  • Better for complex domain logic and functional programming patterns

TypeBox is focused specifically on schema validation:

  • Perfect for “APIs where you need both TypeScript types and standard schema documentation”
  • Works with “any tool that supports JSON Schema”
  • Better TypeScript compile-time performance compared to Zod
  • Excellent for your REST/GraphQL APIs with OpenAPI

Recommendation for Your Stack

Given your preferences (TypeScript, React, REST/GraphQL, AWS CDK, domain-driven approach), TypeBox is likely the better choice because:

  1. Simpler integration - Works seamlessly with OpenAPI specs for your REST APIs
  2. Better performance - Faster runtime validation and better TypeScript compile performance
  3. JSON Schema compatibility - Perfect for API documentation and client generation
  4. Easier adoption - Your team can start using it immediately without functional programming knowledge

Use Effect if:

  • You want to adopt functional programming patterns across your entire application
  • You have complex async workflows and error handling needs
  • You’re willing to invest time in learning FP concepts

Use TypeBox if:

  • You primarily need schema validation for APIs
  • You want something that works well with your existing patterns
  • You value simplicity and immediate productivity

For prefixed IDs specifically, both support custom validation, but TypeBox’s approach is more straightforward for API schemas.