--- title: LLMs Best Practices - Rails + InertiaJS url: https://colingoudie.me/llms-rails.txt description: Rails with InertiaJS conventions for AI-assisted development related: - https://colingoudie.me/llms.txt --- # LLMs Best Practices - Rails + InertiaJS Include this in your CLAUDE.md or similar AI assistant configuration file. See also: https://colingoudie.me/llms.txt ## InertiaJS Response Patterns ### Controller Responses Always use render inertia: - never render json: ```ruby # CORRECT - InertiaJS response def show @project = Project.find(params[:id]) render inertia: "Projects/Show", props: { project: ProjectSerializer.new(@project).as_json } end # WRONG - JSON response breaks Inertia def show @project = Project.find(params[:id]) render json: @project end ``` ### Redirects ```ruby # Standard redirect (Inertia handles it) redirect_to projects_path, notice: "Project created" # With Inertia flash redirect_to projects_path, inertia: { flash: { success: "Created!" } } ``` ### Shared Data ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base inertia_share do { current_user: current_user&.as_json(only: [:id, :name, :email]), flash: flash.to_h } end end ``` ## Controller Conventions ### Thin Controllers ```ruby # GOOD - delegates to service/model class ProjectsController < ApplicationController def create result = Projects::CreateService.call(project_params, current_user) if result.success? redirect_to project_path(result.project), notice: "Created" else redirect_back fallback_location: projects_path, alert: result.error end end end ``` ### Strong Parameters ```ruby private def project_params params.require(:project).permit(:name, :description, :status, tag_ids: []) end ``` ## Serializers ### Use Serializers for All Props ```ruby # app/serializers/project_serializer.rb class ProjectSerializer def initialize(project) @project = project end def as_json { id: @project.id, name: @project.name, description: @project.description, status: @project.status, created_at: @project.created_at.iso8601, user: UserSerializer.new(@project.user).as_json } end end ``` ### Collection Serialization ```ruby def index @projects = Project.includes(:user).page(params[:page]) render inertia: "Projects/Index", props: { projects: @projects.map { |p| ProjectSerializer.new(p).as_json }, pagination: pagination_meta(@projects) } end ``` ## Multi-Tenancy (ActsAsTenant) If using ActsAsTenant: ```ruby # Always scope queries through tenant class Project < ApplicationRecord acts_as_tenant :organisation end # Never bypass tenant scoping # WRONG: Project.unscoped.find(id) # CORRECT: current_tenant.projects.find(id) ``` ## Frontend Components (React/Vue) ### Page Components ```tsx // Pages/Projects/Show.tsx import { usePage } from "@inertiajs/react"; interface Props { project: Project; } export default function Show({ project }: Props) { return (

{project.name}

); } ``` ### Form Handling ```tsx import { useForm } from "@inertiajs/react"; function CreateProject() { const { data, setData, post, processing, errors } = useForm({ name: "", description: "", }); function submit(e: React.FormEvent) { e.preventDefault(); post("/projects"); } return (
setData("name", e.target.value)} /> {errors.name && {errors.name}}
); } ``` ## Service Objects ```ruby # app/services/projects/create_service.rb module Projects class CreateService def self.call(params, user) new(params, user).call end def initialize(params, user) @params = params @user = user end def call project = @user.projects.build(@params) if project.save Result.success(project: project) else Result.failure(error: project.errors.full_messages.join(", ")) end end end end ``` ## Query Objects ```ruby # app/queries/projects_query.rb class ProjectsQuery def initialize(scope = Project.all) @scope = scope end def call(filters = {}) @scope .then { |s| filter_by_status(s, filters[:status]) } .then { |s| filter_by_search(s, filters[:search]) } .then { |s| apply_sorting(s, filters[:sort]) } end private def filter_by_status(scope, status) return scope if status.blank? scope.where(status: status) end end ```