One Source of Truth: Consolidating Our Rails API Contribution Guide for Cursor IDE
Sep 9, 2025We turned scattered conventions into a single PROJECT_CONTRIBUTION_GUIDE_v3.md that your editor and AI assistant can follow. This post explains the structure, the reasoning, and how to use it with Cursor to get consistent, high‑quality code on the first try.
Why consolidate—and why target Cursor?
Keeping rules in several files (rails.mdc
, rspec.mdc
, ruby.mdc
, and a contribution guide) makes context brittle: people and AIs miss details, apply conflicting standards, or burn time reconciling guidance. By merging into a single, sectioned guide, we make expectations discoverable, enforceable, and prompt‑friendly.
- LLM‑ready structure. The guide uses clear categories and subcategories; Cursor can “see” and apply them reliably.
- Less back‑and‑forth. Coders and AI helpers won’t guess response formats or pagination rules.
- First‑class Rails defaults. We start with Rails‑way + KISS, then add just enough structure for teams and CI.
The Canonical Categories (and what they contain)
Below are the categories and representative points you’ll find in the consolidated guide. Each category is designed to be skim‑able for humans and parsable for AI assistants.
1) Philosophy & Principles
- Rails‑way & KISS first. Prefer conventions and built‑ins; avoid layers until complexity demands them.
- Blueprinter is the single JSON source. Controllers never hand‑craft JSON.
- DRY, selectively SOLID. Extract when duplication harms clarity; avoid over‑engineering.
2) Code Style (Ruby)
- Every file starts with:
# frozen_string_literal: true
- Naming:
snake_case
for methods/variables/files,CamelCase
for classes/modules. - Prefer early returns and small, intention‑revealing methods.
- Use UTC internally; serialize timestamps as ISO8601 strings.
3) Routing
- All endpoints are versioned:
/api/v1/
(keeps room for future breaking changes).
4) Controllers
Responses
- Lists:
{ data: [...], meta: { /* pagination & other meta */ } }
- Single item:
{ data: { ... } }
Parameters & Authorization
- Strong parameters everywhere.
- Pundit for authorization (deny by default).
Search & Pagination
- Kaminari for pagination, always include
meta
. - Ransack for search/filters (whitelist searchable attributes).
Attachments in one request
- If an endpoint requires attachments, the model declares associations and accepts nested attributes; the API accepts multipart form data in one round trip.
# frozen_string_literal: true class EducationItem < ApplicationRecord has_many :images, class_name: "GalleryImage", as: :attachable, dependent: :destroy has_many :files, class_name: "FileDocument", as: :attachable, dependent: :destroy accepts_nested_attributes_for :file, allow_destroy: true end
curl -X POST "http://localhost:3000/api/v1/profiles/49/educations" -H "Authorization: Bearer $TOKEN" -H "Content-Type: multipart/form-data" -F "education_item[item_type]=degree" -F "education_item[title]=Bachelor of Science" -F "education_item[degree]=BSc" -F 'education_item[field_of_study]=Computer Science' -F "education_item[currently_enrolled]=false" -F "education_item[start_date]=2018-09-01" -F "education_item[end_date]=2022-06-30" -F "education_item[file_attributes][doc_type]=pdf" -F "education_item[file_attributes][file]=@spec/fixtures/files/test.pdf;type=application/pdf"
Global exception handling
- Centralize error handling in
ApplicationController
to return predictable JSON:render json: { error: e.record.errors.full_messages.to_sentence }, status: :unprocessable_content
5) Models
Validations & Enums
- Keep validations close to data.
- Enums are defined once (DRY) and surfaced verbatim in serializers/tests.
Associations & Attachments
- Use
has_many :images
/has_many :files
as shown above when the endpoint needs attachments. - Use
accepts_nested_attributes_for
to enable single‑request creates/updates.
Constraints
- Database constraints first: NOT NULL, FKs, and unique indexes in migrations—don’t rely solely on model validations.
6) Serialization
- Blueprinter owns the shape of
data
(and error payloads, if you standardize them). Controllers just compose the envelope. - Provide
ApplicationBlueprint
with a smalltimestamps
helper to emit ISO8601created_at/updated_at
.
7) Search / Pagination / Sorting
- Kaminari for limits & pages; add caps and include
meta
in every list response. - Ransack for filters; expose only allow‑listed fields.
- Sorting: allow‑list the columns/direction to prevent injection or invalid columns.
8) Security
- Devise (JWT) for API authentication.
- Pundit for authorization; default deny.
- Strong parameters throughout; no unpermitted mass‑assignment.
- Keep secrets in Rails Credentials; never commit them.
9) Performance & Data
- Avoid N+1 using
includes
/preload
when rendering associated data. - Add indexes for foreign keys and high‑cardinality lookups.
- Consider simple caching (ETag/Last‑Modified) and minimal payloads.
- Light instrumentation via
request_id
tags andActiveSupport::Notifications
.
10) Testing (RSpec)
- Request specs, model specs, policy specs, and serializer (Blueprinter) snapshots.
- FactoryBot + Faker for data.
- Determinism:
freeze_time
/travel_to
for ISO8601 assertions; stub external calls (WebMock/VCR). - Force English locale in tests by default for predictable assertions; add targeted locale tests when needed.
11) Definition of Done (DoD)
- Rails‑way + KISS adhered to.
- Blueprinter for all JSON.
{ data, meta }
for lists;{ data }
for single item.- Pundit + Kaminari + Ransack in place as applicable.
- DB constraints, enums, no N+1.
- Tests for controllers (requests), models, policies, and serialization.
How to attach the Guide to your Cursor prompt (exact steps)
Option A — Replace scattered rules with one file
- Add the consolidated
PROJECT_CONTRIBUTION_GUIDE_v3.md
to your repo. - In Cursor, open your workspace settings and pin this file (or paste it into your global “Rules” file).
- In Chat, reference it explicitly: “Follow the rules in PROJECT_CONTRIBUTION_GUIDE_v3.md. Respect Blueprinter envelopes and the ‘attachments in one request’ policy.”
Option B — Keep rules separate but link to one source
- Keep
rails.mdc
,rspec.mdc
,ruby.mdc
, but add at the top of each: “This file defers to PROJECT_CONTRIBUTION_GUIDE_v3.md; if conflicts arise, the Guide wins.” - Pin all files in Cursor; the Guide becomes the conflict resolver.
Prompt snippet to paste in Cursor (pin this):
You are assisting on a Rails API project. Follow PROJECT_CONTRIBUTION_GUIDE_v3.md strictly:
- Rails-way + KISS; Blueprinter is the single JSON source.
- List endpoints return { data, meta }; single returns { data }.
- Use Pundit for authz, Kaminari for pagination (with meta), Ransack for filters.
- For endpoints with attachments, one request with nested attributes is mandatory.
- Add `# frozen_string_literal: true` at the top of every Ruby file.
- Use UTC + ISO8601 timestamps.
Return only the requested code + file paths; do not invent fields outside the schema.
This pinned prompt reduces retries and keeps generated code aligned with our conventions.
Why this improves efficiency
- Fewer review cycles. Response envelopes, pagination, and authz are consistent from the start.
- Predictable AI output. Cursor synthesizes code that already matches our structure (controllers, models, serializers).
- Lower context cost. One authoritative file means fewer tokens and less ambiguity.
- Safer by default. DB constraints + allow‑listed search/sorting reduce production risks.
- Testable from day 1. RSpec guidance ensures deterministic, CI‑friendly suites.
Quick Reference (copy/paste)
List response pattern
# frozen_string_literal: true
def index
resources = paginate(scope) # Kaminari
render json: { data: Blueprint.render_as_hash(resources), meta: pagination_meta(resources) }
end
Single response pattern
# frozen_string_literal: true
render json: { data: Blueprint.render_as_hash(record) }, status: :created
Global exception example
# frozen_string_literal: true
render json: { error: e.record.errors.full_messages.to_sentence }, status: :unprocessable_content
Attachments in one request (model)
# frozen_string_literal: true
has_many :images, class_name: "GalleryImage", as: :attachable, dependent: :destroy
has_many :files, class_name: "FileDocument", as: :attachable, dependent: :destroy
accepts_nested_attributes_for :file, allow_destroy: true
By unifying our standards and making them prompt‑aware, we get the best of both worlds: Rails defaults that keep us fast, and clear rules that keep humans and AI aligned. The result is higher‑quality code with far less friction.