Principles
When changing this codebase, make sure:
- it has 90% code coverage for backend code
- all tests passing
- meaningful commit messages along the way
Rails commands
bin/rails
is the command to run rails
Admin generation
To generate an admin interface for a model, use the following command:
bin/rails generate admin:scaffold ModelName field1:type field2:type ...
Testing Configuration
- Use FactoryBot for test data
- We must have a valid factory for every model we want to test
- We should have a test for every model
General Architecture
- Write a good controller test for any feature that touches a controller. Consider things like the authenticated state of the user, their ability to perform certain actions, their verified status.
- We should have system tests for crucial flows, but not every feature should be tested via system tests.
Turbo Conventions
- UI relies on Turbo for most interactions, giving an SPA feel for the app
- Follow “Convention over Configuration” for Turbo interactions
- Use consistent Turbo Frame IDs based on the resource relationship hierarchy
- Avoid custom turbo_stream operations when standard redirects can work
Turbo Frame Naming Conventions
Use the frame_id_from_path
helper to derive Turbo frame IDs directly from Rails path helpers:
# For a list of items
<%= turbo_frame_tag frame_id_from_path(consolidation_consignment_items_path(consolidation, consignment)) %>
# For a new item form
<%= turbo_frame_tag frame_id_from_path(new_consolidation_consignment_item_path(consolidation, consignment)) %>
# For a specific item
<%= turbo_frame_tag frame_id_from_path(consolidation_consignment_item_path(consolidation, consignment, item)) %>
# In links pointing to a frame
<%= link_to "Edit",
edit_consolidation_consignment_item_path(consolidation, consignment, item),
data: { turbo_frame: frame_id_from_path(consolidation_consignment_item_path(consolidation, consignment, item)) } %>
# For delete buttons and cancel links, target the parent collection frame (avoids "Content missing" errors)
<%= button_to delete_path(resource),
method: :delete,
form: { data: { turbo_frame: frame_id_from_path(collection_path) } } do %>
<%= lucide_icon "trash-2" %>
<% end %>
# For cancel links in new/edit forms
<%= link_to "Cancel", collection_path %>
This helper automatically converts a Rails path to a consistent turbo frame ID by replacing slashes with underscores:
/consolidations/1/consignments/2/items
becomesconsolidations_1_consignments_2_items
/consolidations/1/consignments/2/items/3/edit
becomesconsolidations_1_consignments_2_items_3_edit
Benefits:
- Frame IDs always match your routes
- If routes change, frame IDs automatically update
- Eliminates manually synchronized IDs
- Creates a predictable pattern across the application
Lazy Loading Content with RESTful Controllers
Use the src
attribute with turbo frames to lazy load content directly from controllers,
avoiding the need for custom partials and explicit renders:
# Lazy load the entire items list from the items controller
<%= turbo_frame_tag frame_id_from_path(consolidation_consignment_items_path(consolidation, consignment)),
src: consolidation_consignment_items_path(consolidation, consignment),
loading: "lazy" do %>
<div class="bg-white rounded-md shadow p-4 text-center text-gray-500 text-sm">
<p>Loading items...</p>
</div>
<% end %>
Benefits:
- Keeps views cleaner without embedded partials
- Allows controllers to handle their own rendering
- Maintains RESTful convention
- Reduces initial page load time
- Provides loading states for better UX
RESTful Controller and Turbo Frame Hierarchy Conventions
- Controllers must remain RESTful and lean - avoid turbo_stream-specific logic
- Always use standard redirects (no specialized turbo_stream renders)
- For nested resources, redirect to the collection path (items_path), not the parent path
- Never create format-specific response blocks unless absolutely necessary
- Let the frame_id_from_path helper and proper routing handle frame updates automatically
Frame Hierarchy Design Pattern
Structure your Turbo Frames to match the RESTful hierarchy:
Controller#index (Main frame: items_path)
├── List of items
└── New item form (Target: new_item_path)
Benefits:
- Clean parent-child relationship between frames
- Navigation (links/redirects) uses standard RESTful routes
- All UI for a resource is managed by its controller
- Redirecting to index (collection path) resets UI state
- Cancel operations simply load index in the same frame
# GOOD - Simple, consistent redirects to collection path
def create
@item = @parent.items.build(item_params)
if @item.save
redirect_to parent_items_path(@parent), notice: "Item created"
else
render :new, status: :unprocessable_entity
end
end
# Form submissions must target parent frame to maintain frame hierarchy
<%= simple_form_for @item,
url: parent_items_path(@parent),
data: {
turbo: true,
turbo_frame: frame_id_from_path(parent_items_path(@parent)) # Point to parent frame
} do |f| %>
<!-- Form fields -->
<% end %>
# BAD - Don't use custom turbo_stream rendering
def create
# ...
respond_to do |format|
format.html { redirect_to parent_path }
format.turbo_stream {
render turbo_stream: turbo_stream.replace(...)
}
end
end
Additional Turbo Tips
- Think twice before using turbo_streams, as they can be complex; prefer turbo frames when possible
- Remember, table rows can’t be wrapped in turbo frames directly - use a grid-based approach or consider building a proper table component
- For deeply nested or complex UI interactions, consider using Stimulus controllers to enhance Turbo Frames
Other architecture
- Using PostgreSQL for our database
- ActiveJob for background jobs
- Where possible encapuslate business logic in services
- I like the pattern where services return a hash with a success boolean and a message
UI Styling
- We use simple_form for our forms. You can use the
simple_form_for
helper to generate a form with our custom form builder. checkdocs/form_layout.md
for info about how to build forms - If you are styling a form, you don’t need to add CSS classes. You can rely on our custom form builder to handle the styling for you
- We use Tailwind CSS for our styling
- we use lucide-rails for icons, you can use the
lucide_icon
helper to render an icon
Search Implementation
- Use Ransack gem for implementing search functionality
- For security, always restrict search to current user’s organization
- Configure models with proper
ransackable_attributes
andransackable_associations
methods - Do not expose organization_id as a searchable attribute for security reasons
- For search forms, use the native Ransack form builder methods (not simple_form):
search_field
for text searchselect
for dropdowns with options_for_select
- Add proper test IDs to search forms:
id="search-field-name"
for search fieldsid="submit-search"
for the submit buttonid="clear-search"
for the clear button
- Always include a clear button for search forms with active filters
- Always test search functionality in both controller and system tests
- For more complex search needs, consider adding custom ransackers to the model
- See
docs/form_layouts.md
for detailed examples of search form implementation