rails_claude_skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.yml +134 -0
  3. data/.github/ISSUE_TEMPLATE/config.yml +11 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.yml +129 -0
  5. data/.github/ISSUE_TEMPLATE/question.yml +90 -0
  6. data/.github/dependabot.yml +19 -0
  7. data/.github/workflows/ci.yml +77 -0
  8. data/.github/workflows/release.yml +66 -0
  9. data/.rubocop.yml +52 -0
  10. data/CHANGELOG.md +94 -0
  11. data/CLAUDE.md +332 -0
  12. data/CODE_OF_CONDUCT.md +134 -0
  13. data/CONTRIBUTING.md +580 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +544 -0
  16. data/Rakefile +8 -0
  17. data/lib/generators/claude/agent/agent_generator.rb +71 -0
  18. data/lib/generators/claude/agent/templates/agent.md.tt +62 -0
  19. data/lib/generators/claude/command/command_generator.rb +50 -0
  20. data/lib/generators/claude/command/templates/command.md.tt +28 -0
  21. data/lib/generators/claude/commands_library/create-pr.md +27 -0
  22. data/lib/generators/claude/commands_library/dbchange.md +19 -0
  23. data/lib/generators/claude/commands_library/quality.md +20 -0
  24. data/lib/generators/claude/commands_library/stimulus.md +19 -0
  25. data/lib/generators/claude/commands_library/turbo-feature.md +17 -0
  26. data/lib/generators/claude/install/install_generator.rb +211 -0
  27. data/lib/generators/claude/install/templates/README.md.tt +59 -0
  28. data/lib/generators/claude/install/templates/USAGE +28 -0
  29. data/lib/generators/claude/install/templates/agents/api-dev.md.tt +46 -0
  30. data/lib/generators/claude/install/templates/agents/fullstack-dev.md.tt +48 -0
  31. data/lib/generators/claude/install/templates/agents/rails-developer.md.tt +40 -0
  32. data/lib/generators/claude/install/templates/settings.local.json.tt +13 -0
  33. data/lib/generators/claude/rule/rule_generator.rb +175 -0
  34. data/lib/generators/claude/rule/templates/rule.md.tt +7 -0
  35. data/lib/generators/claude/rules_library/code-style.md +37 -0
  36. data/lib/generators/claude/rules_library/database.md +47 -0
  37. data/lib/generators/claude/rules_library/hotwire.md +56 -0
  38. data/lib/generators/claude/rules_library/security.md +54 -0
  39. data/lib/generators/claude/rules_library/testing.md +47 -0
  40. data/lib/generators/claude/skill/skill_generator.rb +196 -0
  41. data/lib/generators/claude/skill/templates/SKILL.md.tt +27 -0
  42. data/lib/generators/claude/skills_library/create-task-files/SKILL.md +311 -0
  43. data/lib/generators/claude/skills_library/create-task-files/templates/bug.md +60 -0
  44. data/lib/generators/claude/skills_library/create-task-files/templates/epic.md +47 -0
  45. data/lib/generators/claude/skills_library/create-task-files/templates/issue.md +45 -0
  46. data/lib/generators/claude/skills_library/create-task-files/templates/user-story.md +57 -0
  47. data/lib/generators/claude/skills_library/minitest-testing/SKILL.md +398 -0
  48. data/lib/generators/claude/skills_library/minitest-testing/references/examples.md +889 -0
  49. data/lib/generators/claude/skills_library/plan-feature/SKILL.md +253 -0
  50. data/lib/generators/claude/skills_library/rails-api-controllers/SKILL.md +1041 -0
  51. data/lib/generators/claude/skills_library/rails-api-controllers/references/api-documentation.md +422 -0
  52. data/lib/generators/claude/skills_library/rails-api-controllers/references/serialization.md +456 -0
  53. data/lib/generators/claude/skills_library/rails-auth-with-devise/SKILL.md +191 -0
  54. data/lib/generators/claude/skills_library/rails-auth-with-devise/references/advanced.md +331 -0
  55. data/lib/generators/claude/skills_library/rails-auth-with-devise/references/api-auth.md +266 -0
  56. data/lib/generators/claude/skills_library/rails-auth-with-devise/references/omniauth.md +194 -0
  57. data/lib/generators/claude/skills_library/rails-authorization-cancancan/SKILL.md +603 -0
  58. data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/api-authorization.md +543 -0
  59. data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/complex-permissions.md +572 -0
  60. data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/multi-tenancy.md +373 -0
  61. data/lib/generators/claude/skills_library/rails-controllers/SKILL.md +514 -0
  62. data/lib/generators/claude/skills_library/rails-debugging/SKILL.md +260 -0
  63. data/lib/generators/claude/skills_library/rails-deployment/SKILL.md +437 -0
  64. data/lib/generators/claude/skills_library/rails-deployment/references/examples.md +901 -0
  65. data/lib/generators/claude/skills_library/rails-hotwire/SKILL.md +367 -0
  66. data/lib/generators/claude/skills_library/rails-jobs/MISSION_CONTROL_SETUP.md +639 -0
  67. data/lib/generators/claude/skills_library/rails-jobs/SKILL.md +704 -0
  68. data/lib/generators/claude/skills_library/rails-mailers/SKILL.md +549 -0
  69. data/lib/generators/claude/skills_library/rails-models/SKILL.md +379 -0
  70. data/lib/generators/claude/skills_library/rails-pagination-kaminari/SKILL.md +622 -0
  71. data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/api-pagination.md +523 -0
  72. data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/custom-themes.md +498 -0
  73. data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/performance.md +478 -0
  74. data/lib/generators/claude/skills_library/rails-views/SKILL.md +508 -0
  75. data/lib/generators/claude/skills_library/refine-requirements/SKILL.md +226 -0
  76. data/lib/generators/claude/skills_library/refine-requirements/references/examples.md +344 -0
  77. data/lib/generators/claude/skills_library/refine-requirements/references/reference.md +298 -0
  78. data/lib/generators/claude/skills_library/rspec-testing/SKILL.md +572 -0
  79. data/lib/generators/claude/skills_library/rspec-testing/references/better_specs_guide.md +273 -0
  80. data/lib/generators/claude/skills_library/rspec-testing/references/thoughtbot_patterns.md +407 -0
  81. data/lib/generators/claude/skills_library/tailwindcss/SKILL.md +371 -0
  82. data/lib/generators/claude/views/views_generator.rb +113 -0
  83. data/lib/rails_claude_skills/railtie.rb +16 -0
  84. data/lib/rails_claude_skills/version.rb +5 -0
  85. data/lib/rails_claude_skills.rb +27 -0
  86. data/sig/rails_claude_skills.rbs +4 -0
  87. metadata +199 -0
@@ -0,0 +1,1041 @@
1
+ ---
2
+ name: rails-api-controllers
3
+ description: "RESTful API controller patterns for Ruby on Rails. Use when: (1) Building JSON APIs, (2) API versioning, (3) Error handling and status codes, (4) Authentication with tokens/JWT, (5) Rate limiting, (6) CORS configuration, (7) Pagination and filtering, (8) API documentation, (9) Testing API endpoints"
4
+ ---
5
+
6
+ # Rails API Controllers
7
+
8
+ Build production-ready RESTful JSON APIs with Rails. This skill covers API controller patterns, versioning, authentication, error handling, and best practices for modern API development.
9
+
10
+ <when-to-use>
11
+ - Building JSON APIs for mobile apps, SPAs, or third-party integrations
12
+ - Creating microservices or API-first applications
13
+ - Versioning APIs for backward compatibility
14
+ - Implementing token-based authentication (JWT, API keys)
15
+ - Adding rate limiting and throttling
16
+ - Configuring CORS for cross-origin requests
17
+ - Implementing pagination, filtering, and sorting
18
+ - Testing API endpoints with RSpec
19
+ </when-to-use>
20
+
21
+ <benefits>
22
+ - **RESTful Design** - Follow REST conventions for predictable, maintainable APIs
23
+ - **Proper Status Codes** - Use correct HTTP status codes for all responses
24
+ - **Error Handling** - Consistent error responses with meaningful messages
25
+ - **Versioning** - Support multiple API versions simultaneously
26
+ - **Authentication** - Token-based auth without sessions or cookies
27
+ - **Performance** - Efficient JSON rendering and database queries
28
+ - **Documentation** - Auto-generated API docs with tools like Rswag
29
+ </benefits>
30
+
31
+ <verification-checklist>
32
+ Before completing API controller work:
33
+ - ✅ Proper HTTP status codes used (200, 201, 204, 400, 401, 403, 404, 422, 500)
34
+ - ✅ Consistent JSON response structure
35
+ - ✅ Authentication/authorization implemented
36
+ - ✅ Error handling covers all edge cases
37
+ - ✅ API tests passing (request specs)
38
+ - ✅ CORS configured if needed
39
+ - ✅ Rate limiting configured for production
40
+ - ✅ API documentation generated/updated
41
+ </verification-checklist>
42
+
43
+ <standards>
44
+ - Use `ApplicationController` parent with `ActionController::API` for API-only apps
45
+ - Return proper HTTP status codes for all responses
46
+ - Use consistent JSON structure across all endpoints
47
+ - Implement authentication via tokens (JWT, API keys), NOT sessions
48
+ - Version APIs via URL path (`/api/v1/`) or Accept header
49
+ - Handle errors consistently with JSON error responses
50
+ - Use strong parameters for input validation
51
+ - Test with request specs, not controller specs
52
+ - Document APIs with OpenAPI/Swagger
53
+ - Implement rate limiting to prevent abuse
54
+ </standards>
55
+
56
+ ---
57
+
58
+ ## API-Only Rails Setup
59
+
60
+ <pattern name="api-only-application">
61
+ <description>Create new API-only Rails application</description>
62
+
63
+ **Generate API-Only App:**
64
+
65
+ ```bash
66
+ # New API-only Rails app (skips views, helpers, assets)
67
+ rails new my_api --api
68
+
69
+ # Or add to existing app
70
+ # config/application.rb
71
+ module MyApi
72
+ class Application < Rails::Application
73
+ config.api_only = true
74
+ end
75
+ end
76
+ ```
77
+
78
+ **Base API Controller:**
79
+
80
+ ```ruby
81
+ # app/controllers/application_controller.rb
82
+ class ApplicationController < ActionController::API
83
+ include ActionController::HttpAuthentication::Token::ControllerMethods
84
+
85
+ # Global error handling
86
+ rescue_from ActiveRecord::RecordNotFound, with: :not_found
87
+ rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
88
+ rescue_from ActionController::ParameterMissing, with: :bad_request
89
+
90
+ before_action :authenticate
91
+
92
+ private
93
+
94
+ def authenticate
95
+ authenticate_token || render_unauthorized
96
+ end
97
+
98
+ def authenticate_token
99
+ authenticate_with_http_token do |token, options|
100
+ @current_user = User.find_by(api_token: token)
101
+ end
102
+ end
103
+
104
+ def render_unauthorized
105
+ render json: { error: 'Unauthorized' }, status: :unauthorized
106
+ end
107
+
108
+ def not_found(exception)
109
+ render json: { error: exception.message }, status: :not_found
110
+ end
111
+
112
+ def unprocessable_entity(exception)
113
+ render json: {
114
+ error: 'Validation failed',
115
+ details: exception.record.errors.full_messages
116
+ }, status: :unprocessable_entity
117
+ end
118
+
119
+ def bad_request(exception)
120
+ render json: { error: exception.message }, status: :bad_request
121
+ end
122
+ end
123
+ ```
124
+
125
+ **Why:** API-only mode removes unnecessary middleware and optimizes for JSON responses. Centralized error handling ensures consistent responses.
126
+ </pattern>
127
+
128
+ ---
129
+
130
+ ## RESTful API Design
131
+
132
+ <pattern name="restful-resource-controller">
133
+ <description>Standard RESTful API controller with all CRUD actions</description>
134
+
135
+ ```ruby
136
+ # app/controllers/api/v1/articles_controller.rb
137
+ module Api
138
+ module V1
139
+ class ArticlesController < ApplicationController
140
+ before_action :set_article, only: [:show, :update, :destroy]
141
+
142
+ # GET /api/v1/articles
143
+ def index
144
+ @articles = Article.published
145
+ .includes(:author)
146
+ .page(params[:page])
147
+ .per(params[:per_page] || 20)
148
+
149
+ render json: @articles, status: :ok
150
+ end
151
+
152
+ # GET /api/v1/articles/:id
153
+ def show
154
+ render json: @article, status: :ok
155
+ end
156
+
157
+ # POST /api/v1/articles
158
+ def create
159
+ @article = Article.new(article_params)
160
+ @article.author = current_user
161
+
162
+ if @article.save
163
+ render json: @article, status: :created, location: api_v1_article_url(@article)
164
+ else
165
+ render json: {
166
+ error: 'Failed to create article',
167
+ details: @article.errors.full_messages
168
+ }, status: :unprocessable_entity
169
+ end
170
+ end
171
+
172
+ # PATCH/PUT /api/v1/articles/:id
173
+ def update
174
+ if @article.update(article_params)
175
+ render json: @article, status: :ok
176
+ else
177
+ render json: {
178
+ error: 'Failed to update article',
179
+ details: @article.errors.full_messages
180
+ }, status: :unprocessable_entity
181
+ end
182
+ end
183
+
184
+ # DELETE /api/v1/articles/:id
185
+ def destroy
186
+ @article.destroy
187
+ head :no_content
188
+ end
189
+
190
+ private
191
+
192
+ def set_article
193
+ @article = Article.find(params[:id])
194
+ end
195
+
196
+ def article_params
197
+ params.require(:article).permit(:title, :body, :published)
198
+ end
199
+ end
200
+ end
201
+ end
202
+ ```
203
+
204
+ **Routes:**
205
+
206
+ ```ruby
207
+ # config/routes.rb
208
+ Rails.application.routes.draw do
209
+ namespace :api do
210
+ namespace :v1 do
211
+ resources :articles
212
+ end
213
+ end
214
+ end
215
+ ```
216
+
217
+ **Why:** Follows REST conventions with proper status codes (200 OK, 201 Created, 204 No Content, 422 Unprocessable Entity). Namespace by version for future API changes.
218
+ </pattern>
219
+
220
+ <pattern name="http-status-codes">
221
+ <description>Use correct HTTP status codes for API responses</description>
222
+
223
+ **Common Status Codes:**
224
+
225
+ | Code | Symbol | Usage |
226
+ |------|--------|-------|
227
+ | 200 | `:ok` | Successful GET, PATCH, PUT |
228
+ | 201 | `:created` | Successful POST (resource created) |
229
+ | 204 | `:no_content` | Successful DELETE (no response body) |
230
+ | 400 | `:bad_request` | Invalid request syntax, missing parameters |
231
+ | 401 | `:unauthorized` | Missing or invalid authentication |
232
+ | 403 | `:forbidden` | Authenticated but lacks permission |
233
+ | 404 | `:not_found` | Resource doesn't exist |
234
+ | 422 | `:unprocessable_entity` | Validation errors |
235
+ | 429 | `:too_many_requests` | Rate limit exceeded |
236
+ | 500 | `:internal_server_error` | Server error |
237
+
238
+ **Examples:**
239
+
240
+ ```ruby
241
+ # Success responses
242
+ render json: @article, status: :ok # 200
243
+ render json: @article, status: :created # 201
244
+ head :no_content # 204
245
+
246
+ # Error responses
247
+ render json: { error: 'Bad request' }, status: :bad_request # 400
248
+ render json: { error: 'Unauthorized' }, status: :unauthorized # 401
249
+ render json: { error: 'Forbidden' }, status: :forbidden # 403
250
+ render json: { error: 'Not found' }, status: :not_found # 404
251
+ render json: { error: 'Validation failed' }, status: :unprocessable_entity # 422
252
+ ```
253
+
254
+ **Why:** Correct status codes help API clients handle responses appropriately and provide clear semantics about what happened.
255
+ </pattern>
256
+
257
+ ---
258
+
259
+ ## API Versioning
260
+
261
+ <pattern name="url-versioning">
262
+ <description>Version APIs via URL namespace for backward compatibility</description>
263
+
264
+ **Directory Structure:**
265
+
266
+ ```
267
+ app/controllers/
268
+ └── api/
269
+ ├── v1/
270
+ │ ├── articles_controller.rb
271
+ │ └── users_controller.rb
272
+ └── v2/
273
+ ├── articles_controller.rb
274
+ └── users_controller.rb
275
+ ```
276
+
277
+ **V1 Controller:**
278
+
279
+ ```ruby
280
+ # app/controllers/api/v1/articles_controller.rb
281
+ module Api
282
+ module V1
283
+ class ArticlesController < ApplicationController
284
+ def index
285
+ @articles = Article.all
286
+ render json: @articles
287
+ end
288
+ end
289
+ end
290
+ end
291
+ ```
292
+
293
+ **V2 Controller (Breaking Changes):**
294
+
295
+ ```ruby
296
+ # app/controllers/api/v2/articles_controller.rb
297
+ module Api
298
+ module V2
299
+ class ArticlesController < ApplicationController
300
+ def index
301
+ # V2 adds pagination and filtering
302
+ @articles = Article
303
+ .where(status: params[:status]) if params[:status].present?
304
+ .page(params[:page])
305
+
306
+ render json: {
307
+ data: @articles,
308
+ meta: {
309
+ current_page: @articles.current_page,
310
+ total_pages: @articles.total_pages,
311
+ total_count: @articles.total_count
312
+ }
313
+ }
314
+ end
315
+ end
316
+ end
317
+ end
318
+ ```
319
+
320
+ **Routes:**
321
+
322
+ ```ruby
323
+ # config/routes.rb
324
+ Rails.application.routes.draw do
325
+ namespace :api do
326
+ namespace :v1 do
327
+ resources :articles
328
+ end
329
+
330
+ namespace :v2 do
331
+ resources :articles
332
+ end
333
+ end
334
+ end
335
+ ```
336
+
337
+ **Why:** URL versioning is explicit, easy to test, and allows multiple versions to coexist. Clients can migrate at their own pace.
338
+ </pattern>
339
+
340
+ <antipattern>
341
+ <description>Breaking API changes without versioning</description>
342
+ <bad-example>
343
+
344
+ ```ruby
345
+ # ❌ WRONG - Breaking existing clients
346
+ class Api::ArticlesController < ApplicationController
347
+ def index
348
+ # Changed response structure without versioning
349
+ render json: {
350
+ articles: @articles, # Was just array, now nested
351
+ total: @articles.count # New field
352
+ }
353
+ end
354
+ end
355
+ ```
356
+
357
+ </bad-example>
358
+ <good-example>
359
+
360
+ ```ruby
361
+ # ✅ CORRECT - New version for breaking changes
362
+ module Api
363
+ module V1
364
+ class ArticlesController < ApplicationController
365
+ def index
366
+ render json: @articles # Keep V1 unchanged
367
+ end
368
+ end
369
+ end
370
+
371
+ module V2
372
+ class ArticlesController < ApplicationController
373
+ def index
374
+ render json: {
375
+ articles: @articles,
376
+ total: @articles.count
377
+ }
378
+ end
379
+ end
380
+ end
381
+ end
382
+ ```
383
+
384
+ </good-example>
385
+
386
+ **Why bad:** Breaking changes without versioning break existing API clients. Always version when changing response structure or behavior.
387
+ </antipattern>
388
+
389
+ ---
390
+
391
+ ## Authentication & Authorization
392
+
393
+ <pattern name="token-authentication">
394
+ <description>Token-based authentication for stateless APIs</description>
395
+
396
+ **User Model:**
397
+
398
+ ```ruby
399
+ # app/models/user.rb
400
+ class User < ApplicationRecord
401
+ has_secure_password
402
+ has_secure_token :api_token
403
+
404
+ # Regenerate token on password change
405
+ after_update :regenerate_api_token, if: :saved_change_to_password_digest?
406
+
407
+ private
408
+
409
+ def regenerate_api_token
410
+ regenerate_api_token
411
+ end
412
+ end
413
+ ```
414
+
415
+ **Authentication Controller:**
416
+
417
+ ```ruby
418
+ # app/controllers/api/v1/authentication_controller.rb
419
+ module Api
420
+ module V1
421
+ class AuthenticationController < ApplicationController
422
+ skip_before_action :authenticate, only: [:create]
423
+
424
+ # POST /api/v1/auth
425
+ def create
426
+ user = User.find_by(email: params[:email])
427
+
428
+ if user&.authenticate(params[:password])
429
+ render json: {
430
+ token: user.api_token,
431
+ user: {
432
+ id: user.id,
433
+ email: user.email,
434
+ name: user.name
435
+ }
436
+ }, status: :ok
437
+ else
438
+ render json: { error: 'Invalid email or password' }, status: :unauthorized
439
+ end
440
+ end
441
+
442
+ # DELETE /api/v1/auth
443
+ def destroy
444
+ current_user.regenerate_api_token
445
+ head :no_content
446
+ end
447
+ end
448
+ end
449
+ end
450
+ ```
451
+
452
+ **Using Token in Requests:**
453
+
454
+ ```bash
455
+ # Client sends token in Authorization header
456
+ curl -H "Authorization: Token YOUR_API_TOKEN" \
457
+ https://api.example.com/api/v1/articles
458
+ ```
459
+
460
+ **Why:** Token authentication is stateless (no sessions), works across domains, and is suitable for mobile/SPA clients.
461
+ </pattern>
462
+
463
+ <pattern name="jwt-authentication">
464
+ <description>JWT (JSON Web Token) authentication for APIs</description>
465
+
466
+ **Setup:**
467
+
468
+ ```ruby
469
+ # Gemfile
470
+ gem 'jwt'
471
+
472
+ # lib/json_web_token.rb
473
+ class JsonWebToken
474
+ SECRET_KEY = Rails.application.credentials.secret_key_base
475
+
476
+ def self.encode(payload, exp = 24.hours.from_now)
477
+ payload[:exp] = exp.to_i
478
+ JWT.encode(payload, SECRET_KEY)
479
+ end
480
+
481
+ def self.decode(token)
482
+ body = JWT.decode(token, SECRET_KEY)[0]
483
+ HashWithIndifferentAccess.new(body)
484
+ rescue JWT::DecodeError, JWT::ExpiredSignature
485
+ nil
486
+ end
487
+ end
488
+ ```
489
+
490
+ **Application Controller:**
491
+
492
+ ```ruby
493
+ # app/controllers/application_controller.rb
494
+ class ApplicationController < ActionController::API
495
+ before_action :authenticate_request
496
+
497
+ private
498
+
499
+ def authenticate_request
500
+ header = request.headers['Authorization']
501
+ token = header.split(' ').last if header
502
+ decoded = JsonWebToken.decode(token)
503
+
504
+ if decoded
505
+ @current_user = User.find(decoded[:user_id])
506
+ else
507
+ render json: { error: 'Unauthorized' }, status: :unauthorized
508
+ end
509
+ rescue ActiveRecord::RecordNotFound
510
+ render json: { error: 'Unauthorized' }, status: :unauthorized
511
+ end
512
+
513
+ attr_reader :current_user
514
+ end
515
+ ```
516
+
517
+ **Authentication Endpoint:**
518
+
519
+ ```ruby
520
+ # app/controllers/api/v1/authentication_controller.rb
521
+ module Api
522
+ module V1
523
+ class AuthenticationController < ApplicationController
524
+ skip_before_action :authenticate_request, only: [:create]
525
+
526
+ def create
527
+ user = User.find_by(email: params[:email])
528
+
529
+ if user&.authenticate(params[:password])
530
+ token = JsonWebToken.encode(user_id: user.id)
531
+ render json: { token: token, user: user }, status: :ok
532
+ else
533
+ render json: { error: 'Invalid credentials' }, status: :unauthorized
534
+ end
535
+ end
536
+ end
537
+ end
538
+ end
539
+ ```
540
+
541
+ **Why:** JWT is self-contained, stateless, and can include claims (user_id, roles, expiration). Widely supported by API clients.
542
+ </pattern>
543
+
544
+ ---
545
+
546
+ ## Pagination, Filtering & Sorting
547
+
548
+ <pattern name="pagination">
549
+ <description>Paginate API responses with Kaminari or Pagy</description>
550
+
551
+ **With Kaminari:**
552
+
553
+ ```ruby
554
+ # Gemfile
555
+ gem 'kaminari'
556
+
557
+ # app/controllers/api/v1/articles_controller.rb
558
+ def index
559
+ page = params[:page] || 1
560
+ per_page = params[:per_page] || 20
561
+
562
+ @articles = Article.page(page).per(per_page)
563
+
564
+ render json: {
565
+ data: @articles,
566
+ meta: {
567
+ current_page: @articles.current_page,
568
+ next_page: @articles.next_page,
569
+ prev_page: @articles.prev_page,
570
+ total_pages: @articles.total_pages,
571
+ total_count: @articles.total_count
572
+ }
573
+ }
574
+ end
575
+ ```
576
+
577
+ **With Pagy (Faster):**
578
+
579
+ ```ruby
580
+ # Gemfile
581
+ gem 'pagy'
582
+
583
+ # app/controllers/application_controller.rb
584
+ include Pagy::Backend
585
+
586
+ # app/controllers/api/v1/articles_controller.rb
587
+ def index
588
+ pagy, articles = pagy(Article.all, items: params[:per_page] || 20)
589
+
590
+ render json: {
591
+ data: articles,
592
+ meta: {
593
+ current_page: pagy.page,
594
+ total_pages: pagy.pages,
595
+ total_count: pagy.count,
596
+ per_page: pagy.items
597
+ }
598
+ }
599
+ end
600
+ ```
601
+
602
+ **Why:** Pagination prevents loading large datasets into memory. Include metadata so clients know how to fetch more pages.
603
+ </pattern>
604
+
605
+ <pattern name="filtering-and-sorting">
606
+ <description>Allow clients to filter and sort resources</description>
607
+
608
+ ```ruby
609
+ # app/controllers/api/v1/articles_controller.rb
610
+ def index
611
+ @articles = Article.all
612
+
613
+ # Filtering
614
+ @articles = @articles.where(status: params[:status]) if params[:status].present?
615
+ @articles = @articles.where(category: params[:category]) if params[:category].present?
616
+ @articles = @articles.where('created_at >= ?', params[:from_date]) if params[:from_date].present?
617
+
618
+ # Searching
619
+ @articles = @articles.where('title ILIKE ?', "%#{params[:q]}%") if params[:q].present?
620
+
621
+ # Sorting
622
+ sort_column = params[:sort_by] || 'created_at'
623
+ sort_direction = params[:order] || 'desc'
624
+ @articles = @articles.order("#{sort_column} #{sort_direction}")
625
+
626
+ # Pagination
627
+ @articles = @articles.page(params[:page]).per(params[:per_page] || 20)
628
+
629
+ render json: {
630
+ data: @articles,
631
+ meta: pagination_meta(@articles)
632
+ }
633
+ end
634
+
635
+ private
636
+
637
+ def pagination_meta(collection)
638
+ {
639
+ current_page: collection.current_page,
640
+ total_pages: collection.total_pages,
641
+ total_count: collection.total_count
642
+ }
643
+ end
644
+ ```
645
+
646
+ **Example Requests:**
647
+
648
+ ```bash
649
+ # Filter by status
650
+ GET /api/v1/articles?status=published
651
+
652
+ # Search by title
653
+ GET /api/v1/articles?q=rails
654
+
655
+ # Sort by created_at descending
656
+ GET /api/v1/articles?sort_by=created_at&order=desc
657
+
658
+ # Combine filters, search, sort, and pagination
659
+ GET /api/v1/articles?status=published&q=rails&sort_by=title&order=asc&page=2&per_page=50
660
+ ```
661
+
662
+ **Why:** Flexible filtering and sorting let clients fetch exactly what they need without loading unnecessary data.
663
+ </pattern>
664
+
665
+ ---
666
+
667
+ ## CORS Configuration
668
+
669
+ <pattern name="cors-setup">
670
+ <description>Configure CORS to allow cross-origin API requests</description>
671
+
672
+ **Setup:**
673
+
674
+ ```ruby
675
+ # Gemfile
676
+ gem 'rack-cors'
677
+
678
+ # config/initializers/cors.rb
679
+ Rails.application.config.middleware.insert_before 0, Rack::Cors do
680
+ allow do
681
+ origins 'example.com', 'localhost:3000' # Whitelist specific origins
682
+
683
+ resource '/api/*',
684
+ headers: :any,
685
+ methods: [:get, :post, :put, :patch, :delete, :options, :head],
686
+ credentials: true,
687
+ max_age: 86400 # Cache preflight for 24 hours
688
+ end
689
+ end
690
+ ```
691
+
692
+ **Development (Allow All Origins):**
693
+
694
+ ```ruby
695
+ # config/initializers/cors.rb
696
+ Rails.application.config.middleware.insert_before 0, Rack::Cors do
697
+ allow do
698
+ if Rails.env.development?
699
+ origins '*' # Allow all in development
700
+ else
701
+ origins ENV['ALLOWED_ORIGINS']&.split(',') || 'example.com'
702
+ end
703
+
704
+ resource '/api/*',
705
+ headers: :any,
706
+ methods: [:get, :post, :put, :patch, :delete, :options, :head]
707
+ end
708
+ end
709
+ ```
710
+
711
+ **Why:** CORS is required when frontend (SPA, mobile app) and API are on different domains. Whitelist specific origins in production for security.
712
+ </pattern>
713
+
714
+ ---
715
+
716
+ ## Rate Limiting
717
+
718
+ <pattern name="rate-limiting">
719
+ <description>Implement rate limiting to prevent API abuse</description>
720
+
721
+ **With Rack::Attack:**
722
+
723
+ ```ruby
724
+ # Gemfile
725
+ gem 'rack-attack'
726
+
727
+ # config/initializers/rack_attack.rb
728
+ class Rack::Attack
729
+ # Throttle all requests by IP (60 requests per minute)
730
+ throttle('req/ip', limit: 60, period: 1.minute) do |req|
731
+ req.ip if req.path.start_with?('/api/')
732
+ end
733
+
734
+ # Throttle POST requests by IP (10 per minute)
735
+ throttle('req/ip/post', limit: 10, period: 1.minute) do |req|
736
+ req.ip if req.path.start_with?('/api/') && req.post?
737
+ end
738
+
739
+ # Throttle authenticated requests by user token
740
+ throttle('req/token', limit: 100, period: 1.minute) do |req|
741
+ if req.path.start_with?('/api/')
742
+ token = req.env['HTTP_AUTHORIZATION']&.split(' ')&.last
743
+ User.find_by(api_token: token)&.id if token
744
+ end
745
+ end
746
+
747
+ # Custom response for throttled requests
748
+ self.throttled_responder = lambda do |env|
749
+ [
750
+ 429,
751
+ { 'Content-Type' => 'application/json' },
752
+ [{ error: 'Rate limit exceeded. Try again later.' }.to_json]
753
+ ]
754
+ end
755
+ end
756
+
757
+ # config/application.rb
758
+ config.middleware.use Rack::Attack
759
+ ```
760
+
761
+ **Why:** Rate limiting prevents abuse, protects server resources, and ensures fair usage across all API clients.
762
+ </pattern>
763
+
764
+ ---
765
+
766
+ ## Error Handling
767
+
768
+ <pattern name="consistent-error-responses">
769
+ <description>Standardized error response format</description>
770
+
771
+ ```ruby
772
+ # app/controllers/application_controller.rb
773
+ class ApplicationController < ActionController::API
774
+ rescue_from StandardError, with: :internal_server_error
775
+ rescue_from ActiveRecord::RecordNotFound, with: :not_found
776
+ rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
777
+ rescue_from ActionController::ParameterMissing, with: :bad_request
778
+ rescue_from Pundit::NotAuthorizedError, with: :forbidden
779
+
780
+ private
781
+
782
+ def not_found(exception)
783
+ render json: error_response(
784
+ 'Resource not found',
785
+ exception.message
786
+ ), status: :not_found
787
+ end
788
+
789
+ def unprocessable_entity(exception)
790
+ render json: error_response(
791
+ 'Validation failed',
792
+ exception.record.errors.full_messages
793
+ ), status: :unprocessable_entity
794
+ end
795
+
796
+ def bad_request(exception)
797
+ render json: error_response(
798
+ 'Bad request',
799
+ exception.message
800
+ ), status: :bad_request
801
+ end
802
+
803
+ def forbidden(exception)
804
+ render json: error_response(
805
+ 'Forbidden',
806
+ 'You are not authorized to perform this action'
807
+ ), status: :forbidden
808
+ end
809
+
810
+ def internal_server_error(exception)
811
+ # Log error for debugging
812
+ Rails.logger.error(exception.message)
813
+ Rails.logger.error(exception.backtrace.join("\n"))
814
+
815
+ render json: error_response(
816
+ 'Internal server error',
817
+ Rails.env.production? ? 'Something went wrong' : exception.message
818
+ ), status: :internal_server_error
819
+ end
820
+
821
+ def error_response(message, details = nil)
822
+ response = { error: message }
823
+ response[:details] = details if details.present?
824
+ response
825
+ end
826
+ end
827
+ ```
828
+
829
+ **Example Error Responses:**
830
+
831
+ ```json
832
+ // 404 Not Found
833
+ {
834
+ "error": "Resource not found",
835
+ "details": "Couldn't find Article with 'id'=999"
836
+ }
837
+
838
+ // 422 Unprocessable Entity
839
+ {
840
+ "error": "Validation failed",
841
+ "details": [
842
+ "Title can't be blank",
843
+ "Body is too short (minimum is 10 characters)"
844
+ ]
845
+ }
846
+
847
+ // 400 Bad Request
848
+ {
849
+ "error": "Bad request",
850
+ "details": "param is missing or the value is empty: article"
851
+ }
852
+ ```
853
+
854
+ **Why:** Consistent error format makes it easy for clients to parse and display errors. Include details for debugging without exposing sensitive info.
855
+ </pattern>
856
+
857
+ ---
858
+
859
+ ## Testing API Endpoints
860
+
861
+ <pattern name="request-specs">
862
+ <description>Test API endpoints with RSpec request specs</description>
863
+
864
+ ```ruby
865
+ # spec/requests/api/v1/articles_spec.rb
866
+ require 'rails_helper'
867
+
868
+ RSpec.describe 'Api::V1::Articles', type: :request do
869
+ let(:user) { create(:user) }
870
+ let(:headers) { { 'Authorization' => "Token #{user.api_token}" } }
871
+
872
+ describe 'GET /api/v1/articles' do
873
+ let!(:articles) { create_list(:article, 3, :published) }
874
+
875
+ it 'returns all published articles' do
876
+ get '/api/v1/articles', headers: headers
877
+
878
+ expect(response).to have_http_status(:ok)
879
+ expect(json_response['data'].size).to eq(3)
880
+ end
881
+
882
+ it 'filters by status' do
883
+ draft = create(:article, status: :draft)
884
+
885
+ get '/api/v1/articles', params: { status: 'draft' }, headers: headers
886
+
887
+ expect(response).to have_http_status(:ok)
888
+ expect(json_response['data'].size).to eq(1)
889
+ expect(json_response['data'].first['id']).to eq(draft.id)
890
+ end
891
+
892
+ it 'paginates results' do
893
+ create_list(:article, 25)
894
+
895
+ get '/api/v1/articles', params: { page: 2, per_page: 10 }, headers: headers
896
+
897
+ expect(response).to have_http_status(:ok)
898
+ expect(json_response['data'].size).to eq(10)
899
+ expect(json_response['meta']['current_page']).to eq(2)
900
+ end
901
+ end
902
+
903
+ describe 'POST /api/v1/articles' do
904
+ let(:valid_attributes) { { article: { title: 'Test', body: 'Content' } } }
905
+
906
+ it 'creates a new article' do
907
+ expect {
908
+ post '/api/v1/articles', params: valid_attributes, headers: headers
909
+ }.to change(Article, :count).by(1)
910
+
911
+ expect(response).to have_http_status(:created)
912
+ expect(json_response['title']).to eq('Test')
913
+ expect(response.location).to be_present
914
+ end
915
+
916
+ it 'returns errors for invalid data' do
917
+ post '/api/v1/articles', params: { article: { title: '' } }, headers: headers
918
+
919
+ expect(response).to have_http_status(:unprocessable_entity)
920
+ expect(json_response['error']).to eq('Failed to create article')
921
+ expect(json_response['details']).to include("Title can't be blank")
922
+ end
923
+ end
924
+
925
+ describe 'DELETE /api/v1/articles/:id' do
926
+ let!(:article) { create(:article) }
927
+
928
+ it 'deletes the article' do
929
+ expect {
930
+ delete "/api/v1/articles/#{article.id}", headers: headers
931
+ }.to change(Article, :count).by(-1)
932
+
933
+ expect(response).to have_http_status(:no_content)
934
+ expect(response.body).to be_empty
935
+ end
936
+ end
937
+
938
+ describe 'authentication' do
939
+ it 'returns 401 without token' do
940
+ get '/api/v1/articles'
941
+
942
+ expect(response).to have_http_status(:unauthorized)
943
+ expect(json_response['error']).to eq('Unauthorized')
944
+ end
945
+
946
+ it 'returns 401 with invalid token' do
947
+ get '/api/v1/articles', headers: { 'Authorization' => 'Token invalid' }
948
+
949
+ expect(response).to have_http_status(:unauthorized)
950
+ end
951
+ end
952
+
953
+ private
954
+
955
+ def json_response
956
+ JSON.parse(response.body)
957
+ end
958
+ end
959
+ ```
960
+
961
+ **Why:** Request specs test the full HTTP request/response cycle including routing, authentication, and JSON parsing. More realistic than controller specs.
962
+ </pattern>
963
+
964
+ ---
965
+
966
+ <testing>
967
+
968
+ ```ruby
969
+ # spec/support/request_helpers.rb
970
+ module RequestHelpers
971
+ def json_response
972
+ JSON.parse(response.body)
973
+ end
974
+
975
+ def auth_headers(user)
976
+ { 'Authorization' => "Token #{user.api_token}" }
977
+ end
978
+ end
979
+
980
+ RSpec.configure do |config|
981
+ config.include RequestHelpers, type: :request
982
+ end
983
+
984
+ # spec/requests/api/v1/authentication_spec.rb
985
+ RSpec.describe 'Api::V1::Authentication', type: :request do
986
+ describe 'POST /api/v1/auth' do
987
+ let(:user) { create(:user, email: 'test@example.com', password: 'password') }
988
+
989
+ it 'returns token with valid credentials' do
990
+ post '/api/v1/auth', params: { email: 'test@example.com', password: 'password' }
991
+
992
+ expect(response).to have_http_status(:ok)
993
+ expect(json_response['token']).to be_present
994
+ expect(json_response['user']['email']).to eq('test@example.com')
995
+ end
996
+
997
+ it 'returns error with invalid credentials' do
998
+ post '/api/v1/auth', params: { email: 'test@example.com', password: 'wrong' }
999
+
1000
+ expect(response).to have_http_status(:unauthorized)
1001
+ expect(json_response['error']).to eq('Invalid email or password')
1002
+ end
1003
+ end
1004
+ end
1005
+ ```
1006
+
1007
+ </testing>
1008
+
1009
+ ---
1010
+
1011
+ <related-skills>
1012
+ - rails-ai:models - Model patterns for API resources
1013
+ - rails-ai:serializers - JSON serialization (ActiveModelSerializers, Blueprinter)
1014
+ - rails-ai:testing - Testing patterns for API endpoints
1015
+ - rails-ai:auth-with-devise - Token-based authentication with Devise
1016
+ - rails-ai:jobs - Background processing for async API operations
1017
+ </related-skills>
1018
+
1019
+ <resources>
1020
+
1021
+ **Official Documentation:**
1022
+ - [Rails Guides - API-Only Applications](https://guides.rubyonrails.org/api_app.html)
1023
+ - [Rails API Documentation](https://api.rubyonrails.org/)
1024
+
1025
+ **Gems & Libraries:**
1026
+ - [jwt](https://github.com/jwt/ruby-jwt) - JSON Web Token implementation
1027
+ - [rack-cors](https://github.com/cyu/rack-cors) - CORS middleware
1028
+ - [rack-attack](https://github.com/rack/rack-attack) - Rate limiting and throttling
1029
+ - [kaminari](https://github.com/kaminari/kaminari) - Pagination
1030
+ - [pagy](https://github.com/ddnexus/pagy) - Fast pagination
1031
+ - [pundit](https://github.com/varvet/pundit) - Authorization
1032
+
1033
+ **API Documentation:**
1034
+ - [rswag](https://github.com/rswag/rswag) - OpenAPI/Swagger docs for Rails APIs
1035
+ - [apipie-rails](https://github.com/Apipie/apipie-rails) - API documentation tool
1036
+
1037
+ **Best Practices:**
1038
+ - [REST API Tutorial](https://restfulapi.net/)
1039
+ - [HTTP Status Codes](https://httpstatuses.com/)
1040
+
1041
+ </resources>