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 expectsfunction 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 specconst 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 stringtype SubscriptionID string
func (c CustomerID) Validate() error { if !strings.HasPrefix(string(c), "cus_") { return errors.New("invalid customer ID format") } return nil}
// With Gin frameworkfunc 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 validatorfunc 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:
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 generationexport 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:
- Simpler integration - Works seamlessly with OpenAPI specs for your REST APIs
- Better performance - Faster runtime validation and better TypeScript compile performance
- JSON Schema compatibility - Perfect for API documentation and client generation
- 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.