export type PostId = string & { readonly brand: unique symbol }
export type UserId = string & { readonly brand: unique symbol }
export interface User {
id: UserId
name: string
email: string
avatar_url: string | null
}
export type PostStatus = 'draft' | 'published' | 'archived'
export interface Post {
id: PostId
title: string
body: string
excerpt: string
status: PostStatus
published_at: string | null
created_at: string
updated_at: string
author: User
tags: string[]
comments_count: number
likes_count: number
}
export interface PostFormData {
title: string
body: string
status: PostStatus
tags: string[]
}
export interface PaginatedResponse<T> {
data: T[]
meta: {
current_page: number
total_pages: number
total_count: number
per_page: number
}
}
Keeping TypeScript types in sync with Rails API responses is critical but tedious. I generate TypeScript interfaces automatically from Rails serializers or JSON Schema using tools like quicktype or custom scripts. For manual definitions, I create a types directory that mirrors the Rails model structure. Each type includes all attributes from the serializer plus computed fields. I use branded types for IDs to prevent mixing up user IDs with post IDs. Unions and literal types represent Rails enums. The key discipline is updating types whenever the API shape changes—I catch this in code review and with integration tests that verify response shapes. Proper types catch API contract violations at compile time instead of runtime.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.