Project

zodra

0.0
No release in over 3 years
Define types once in Ruby DSL, generate TypeScript interfaces and Zod schemas for runtime validation on both ends.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 7.1
>= 2.6
 Project Readme

Zodra

End-to-end type system for Rails
Define types once in Ruby DSL. Generate TypeScript interfaces and Zod schemas for your frontend.

CI License

Documentation · Quick Start · GitHub


Overview

Zodra is a full API framework for Rails that bridges the gap between backend and frontend type safety. You define your types, contracts, and API structure in a Ruby DSL — Zodra handles validation, serialization, routing, and code generation.

One definition, both sides:

Ruby DSL  →  TypeScript interfaces  +  Zod schemas
          →  Params validation      +  Response serialization
          →  Resource routing       +  Controller helpers

Features

  • Type DSL — objects, enums, unions with full attribute support (optional, nullable, defaults, constraints)
  • Type Composition — derive types with from:, pick:, omit:, partial: (like TypeScript utility types)
  • Contracts — define params and responses per action, decoupled from routing
  • Resource RoutingZodra.api maps contracts to RESTful routes with nested resources and custom actions
  • Params Validation — strict by default, coerces and validates incoming params against contract schemas
  • Response Serialization — consistent { data: ... } / { data: [...], meta: ... } envelope
  • TypeScript Export — generates .ts interfaces from your type definitions
  • Zod Export — generates .ts schemas with constraints (z.string().min(1), z.number().int())
  • Controller Mixininclude Zodra::Controller for zodra_params, zodra_respond, error handling
  • OpenAPI 3.1 — auto-generated specs from your contracts with rake zodra:openapi
  • Swagger UI — built-in interactive API docs at /docs with mount Zodra::Swagger
  • Rails Native — Railtie, rake tasks, works with Zeitwerk and standard Rails conventions

Quick Start

Installation

Add to your Gemfile:

gem "zodra"

1. Define a Type

# app/types/product.rb
Zodra.type :product do
  uuid    :id
  string  :name
  string  :sku
  decimal :price
  integer :stock
  boolean :published
end

2. Define a Contract

# app/contracts/products.rb
Zodra.contract :products do
  action :index do
    response :product, collection: true
  end

  action :show do
    params do
      uuid :id
    end
    response :product
  end

  action :create do
    params do
      string  :name, min: 1
      string  :sku, min: 1
      decimal :price, min: 0
      integer :stock, min: 0
      boolean :published, default: false
    end
    response :product
  end

  action :update do
    params do
      uuid     :id
      string?  :name, min: 1
      string?  :sku, min: 1
      decimal? :price, min: 0
      integer? :stock, min: 0
      boolean? :published
    end
    response :product
  end

  action :destroy do
    params do
      uuid :id
    end
  end
end

3. Define the API

# config/apis/v1.rb
Zodra.api "/api/v1" do
  resources :products
end

4. Set Up Routes

# config/routes.rb
Rails.application.routes.draw do
  mount Zodra::Swagger => '/docs'  # Swagger UI at /docs
  zodra_routes
end

5. Write the Controller

# app/controllers/api/v1/products_controller.rb
module Api
  module V1
    class ProductsController < ApplicationController
      include Zodra::Controller

      zodra_contract :products

      def index
        products = Product.all
        zodra_respond_collection(products)
      end

      def show
        product = Product.find(zodra_params[:id])
        zodra_respond(product)
      end

      def create
        product = Product.create!(zodra_params)
        zodra_respond(product, status: :created)
      end

      def update
        product = Product.find(zodra_params[:id])
        product.update!(zodra_params.except(:id))
        zodra_respond(product)
      end

      def destroy
        product = Product.find(zodra_params[:id])
        product.destroy!
        head :no_content
      end
    end
  end
end

6. Export to TypeScript + Zod

bin/rails zodra:export
# Generated app/javascript/types/schemas.ts
# Generated app/javascript/types/types.ts

Generated TypeScript:

export interface Product {
  id: string;
  name: string;
  sku: string;
  price: number;
  stock: number;
  published: boolean;
}

Generated Zod:

import { z } from 'zod';

export const ProductSchema = z.object({
  id: z.uuid(),
  name: z.string(),
  sku: z.string(),
  price: z.number(),
  stock: z.number().int(),
  published: z.boolean(),
});

Type DSL

Primitive Types

DSL TypeScript Zod
string string z.string()
integer number z.number().int()
decimal number z.number()
boolean boolean z.boolean()
uuid string z.uuid()
date string z.iso.date()
datetime string z.iso.datetime()

Modifiers

string? :nickname           # optional
string :name, nullable: true # nullable
string :role, default: "user"
string :name, min: 1, max: 100

Enums

Zodra.enum :status, values: %w[draft published archived]

Unions

Zodra.union :payment, discriminator: :type do
  variant :card do
    string :last_four
  end
  variant :bank_transfer do
    string :iban
  end
end

References and Arrays

Zodra.type :order do
  uuid :id
  reference :customer       # references Zodra.type :customer
  array :items, of: :line_item  # array of Zodra.type :line_item
end

Type Composition

Derive new types from existing ones — like TypeScript's Pick, Omit, and Partial:

Zodra.type :create_product_params, from: :product, omit: [:id]
Zodra.type :update_product_params, from: :product, omit: [:id], partial: true
Zodra.type :product_summary, from: :product, pick: [:id, :name, :price]

Attribute Blocks

Blocks replace the default value extraction in ResponseSerializer. Use them when the attribute value comes from a different source than the object's method:

Zodra.type :clock_in_day do
  integer :id
  string :date do |day|
    UiDate.format(day.date)
  end

  boolean :can_edit do |day, context|
    context[:ability].can?(:edit, day)
  end

  reference :staff_member, to: :staff do |day|
    StaffPresenter.new(day.staff_member)
  end
end

Pass context through ResponseSerializer.call(object, definition, context: { ability: current_ability }).

Configuration

# config/initializers/zodra.rb
Zodra.configure do |config|
  config.output_path = "app/javascript/types" # where to write generated files
  config.key_format = :camel                  # :camel, :pascal, or :keep
  config.zod_import = "zod"                   # import path for Zod
  config.strict_params = true                 # reject unknown params
  config.strict_serialization = true          # raise on missing required attributes

  # OpenAPI / Swagger UI
  config.openapi_title = "My API"
  config.openapi_version = "1.0.0"
  config.openapi_description = "API documentation"
end

Rake Tasks

bin/rails zodra:export              # generate both TypeScript + Zod
bin/rails zodra:export:typescript   # generate only TypeScript interfaces
bin/rails zodra:export:zod          # generate only Zod schemas
bin/rails zodra:openapi             # generate OpenAPI 3.1 JSON specs

Project Structure

zodra/
├── gem/                    # Ruby gem (zodra)
│   ├── lib/zodra/          # Core library
│   └── spec/               # RSpec tests
├── js/                     # JavaScript packages (pnpm workspace)
│   └── packages/
│       ├── client/         # @zodra/client
│       └── vscode/         # VS Code extension
├── website/                # Next.js documentation (zodra.dev)
├── example/                # Full Rails API app (smoke test)
└── .github/workflows/      # CI (Ruby 3.2/3.3/3.4 + Node 22)

Development

Requirements

  • Ruby >= 3.2
  • Node.js >= 22
  • pnpm >= 10

Ruby (gem)

cd gem
bundle install
bundle exec rspec        # run tests
bundle exec rubocop      # lint

JavaScript (packages)

cd js
pnpm install
pnpm test                # run tests
pnpm typecheck           # type check

Example App

cd example
bundle install
bin/rails db:create db:migrate db:seed
bin/rails server
# CRUD: curl http://localhost:3000/api/v1/products

License

MIT