funapi 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/.claude/25-09-01-OPENAPI_IMPLEMENTATION.md +233 -0
  3. data/.claude/25-09-05-RESPONSE_SCHEMA.md +383 -0
  4. data/.claude/25-09-10-OPENAPI_PLAN.md +219 -0
  5. data/.claude/25-10-26-MIDDLEWARE_IMPLEMENTATION.md +230 -0
  6. data/.claude/25-10-26-MIDDLEWARE_PLAN.md +353 -0
  7. data/.claude/25-10-27-BACKGROUND_TASKS_ANALYSIS.md +325 -0
  8. data/.claude/25-10-27-DEPENDENCY_IMPLEMENTATION_SUMMARY.md +325 -0
  9. data/.claude/25-10-27-DEPENDENCY_INJECTION_PLAN.md +753 -0
  10. data/.claude/25-12-24-LIFECYCLE_HOOKS_PLAN.md +421 -0
  11. data/.claude/25-12-24-PUBLISHING_AND_DOGFOODING_PLAN.md +327 -0
  12. data/.claude/25-12-24-TEMPLATE_RENDERING_PLAN.md +704 -0
  13. data/.claude/DECISIONS.md +397 -0
  14. data/.claude/PROJECT_PLAN.md +80 -0
  15. data/.claude/TESTING_PLAN.md +285 -0
  16. data/.claude/TESTING_STATUS.md +157 -0
  17. data/.tool-versions +1 -0
  18. data/AGENTS.md +416 -0
  19. data/CHANGELOG.md +5 -0
  20. data/CODE_OF_CONDUCT.md +132 -0
  21. data/LICENSE.txt +21 -0
  22. data/README.md +660 -0
  23. data/Rakefile +10 -0
  24. data/docs +8 -0
  25. data/docs-site/.gitignore +3 -0
  26. data/docs-site/Gemfile +9 -0
  27. data/docs-site/app.rb +138 -0
  28. data/docs-site/content/essential/handler.md +156 -0
  29. data/docs-site/content/essential/lifecycle.md +161 -0
  30. data/docs-site/content/essential/middleware.md +201 -0
  31. data/docs-site/content/essential/openapi.md +155 -0
  32. data/docs-site/content/essential/routing.md +123 -0
  33. data/docs-site/content/essential/validation.md +166 -0
  34. data/docs-site/content/getting-started/at-glance.md +82 -0
  35. data/docs-site/content/getting-started/key-concepts.md +150 -0
  36. data/docs-site/content/getting-started/quick-start.md +127 -0
  37. data/docs-site/content/index.md +81 -0
  38. data/docs-site/content/patterns/async-operations.md +137 -0
  39. data/docs-site/content/patterns/background-tasks.md +143 -0
  40. data/docs-site/content/patterns/database.md +175 -0
  41. data/docs-site/content/patterns/dependencies.md +141 -0
  42. data/docs-site/content/patterns/deployment.md +212 -0
  43. data/docs-site/content/patterns/error-handling.md +184 -0
  44. data/docs-site/content/patterns/response-schema.md +159 -0
  45. data/docs-site/content/patterns/templates.md +193 -0
  46. data/docs-site/content/patterns/testing.md +218 -0
  47. data/docs-site/mise.toml +2 -0
  48. data/docs-site/public/css/style.css +234 -0
  49. data/docs-site/templates/layouts/docs.html.erb +28 -0
  50. data/docs-site/templates/page.html.erb +3 -0
  51. data/docs-site/templates/partials/_nav.html.erb +19 -0
  52. data/examples/background_tasks_demo.rb +159 -0
  53. data/examples/demo_middleware.rb +55 -0
  54. data/examples/demo_openapi.rb +63 -0
  55. data/examples/dependency_block_demo.rb +150 -0
  56. data/examples/dependency_cleanup_demo.rb +146 -0
  57. data/examples/dependency_injection_demo.rb +200 -0
  58. data/examples/lifecycle_demo.rb +57 -0
  59. data/examples/middleware_demo.rb +74 -0
  60. data/examples/templates/layouts/application.html.erb +66 -0
  61. data/examples/templates/todos/_todo.html.erb +15 -0
  62. data/examples/templates/todos/index.html.erb +12 -0
  63. data/examples/templates_demo.rb +87 -0
  64. data/lib/funapi/application.rb +521 -0
  65. data/lib/funapi/async.rb +57 -0
  66. data/lib/funapi/background_tasks.rb +52 -0
  67. data/lib/funapi/config.rb +23 -0
  68. data/lib/funapi/database/sequel/fibered_connection_pool.rb +87 -0
  69. data/lib/funapi/dependency_wrapper.rb +66 -0
  70. data/lib/funapi/depends.rb +138 -0
  71. data/lib/funapi/exceptions.rb +72 -0
  72. data/lib/funapi/middleware/base.rb +13 -0
  73. data/lib/funapi/middleware/cors.rb +23 -0
  74. data/lib/funapi/middleware/request_logger.rb +32 -0
  75. data/lib/funapi/middleware/trusted_host.rb +34 -0
  76. data/lib/funapi/middleware.rb +4 -0
  77. data/lib/funapi/openapi/schema_converter.rb +85 -0
  78. data/lib/funapi/openapi/spec_generator.rb +179 -0
  79. data/lib/funapi/router.rb +43 -0
  80. data/lib/funapi/schema.rb +65 -0
  81. data/lib/funapi/server/falcon.rb +38 -0
  82. data/lib/funapi/template_response.rb +17 -0
  83. data/lib/funapi/templates.rb +111 -0
  84. data/lib/funapi/version.rb +5 -0
  85. data/lib/funapi.rb +14 -0
  86. data/sig/fun_api.rbs +499 -0
  87. metadata +220 -0
@@ -0,0 +1,353 @@
1
+ # Middleware Implementation Plan
2
+
3
+ ## Goal
4
+
5
+ Implement a comprehensive middleware system for FunApi that leverages the battle-tested Rack middleware ecosystem while providing FastAPI-like convenience methods.
6
+
7
+ ## Architecture Decision: Hybrid Approach
8
+
9
+ Support both standard Rack middleware AND FunApi-specific convenience wrappers:
10
+
11
+ ```ruby
12
+ # 1. Standard Rack middleware (existing ecosystem)
13
+ app = FunApi::App.new do |api|
14
+ api.use Rack::Cors do |config|
15
+ config.allow do |allow|
16
+ allow.origins '*'
17
+ allow.resource '*', headers: :any, methods: [:get, :post]
18
+ end
19
+ end
20
+ end
21
+
22
+ # 2. FunApi-specific middleware (FastAPI-style convenience)
23
+ app = FunApi::App.new do |api|
24
+ api.add_cors(
25
+ allow_origins: ['*'],
26
+ allow_methods: ['*'],
27
+ allow_headers: ['*']
28
+ )
29
+
30
+ # Or custom FunApi middleware with async support
31
+ api.use FunApi::Middleware::RateLimit, max_requests: 100, window: 60
32
+ end
33
+ ```
34
+
35
+ ## Implementation Phases
36
+
37
+ ### Phase 1: Core Middleware System ⭐⭐⭐⭐⭐
38
+
39
+ **File**: `lib/fun_api/application.rb`
40
+
41
+ **Changes**:
42
+ 1. Uncomment and fix `build_middleware_chain` method (lines 144-152)
43
+ 2. Add `use(middleware, *args, &block)` method
44
+ 3. Update `call(env)` to use middleware chain instead of router directly
45
+ 4. Ensure middleware is applied in correct order (LIFO - Last In, First Out)
46
+
47
+ **Why First**: This unblocks everything else and enables the entire Rack ecosystem.
48
+
49
+ **Implementation**:
50
+ ```ruby
51
+ class App
52
+ def use(middleware, *args, &block)
53
+ @middleware_stack << [middleware, args, block]
54
+ self
55
+ end
56
+
57
+ def call(env)
58
+ app = build_middleware_chain
59
+ app.call(env)
60
+ end
61
+
62
+ private
63
+
64
+ def build_middleware_chain
65
+ # Start with router as the innermost app
66
+ app = @router
67
+
68
+ # Wrap with middleware in reverse order (LIFO)
69
+ @middleware_stack.reverse_each do |middleware, args, block|
70
+ app = middleware.new(app, *args, &block)
71
+ end
72
+
73
+ app
74
+ end
75
+ end
76
+ ```
77
+
78
+ ### Phase 2: Built-in Middleware ⭐⭐⭐⭐
79
+
80
+ Create FunApi-specific middleware with async awareness and FastAPI-style convenience.
81
+
82
+ #### A. Base Middleware Class
83
+ **File**: `lib/fun_api/middleware/base.rb`
84
+
85
+ ```ruby
86
+ module FunApi
87
+ module Middleware
88
+ class Base
89
+ def initialize(app)
90
+ @app = app
91
+ end
92
+
93
+ def call(env)
94
+ @app.call(env)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ ```
100
+
101
+ #### B. CORS Middleware (Delegate to rack-cors)
102
+ **File**: `lib/fun_api/middleware/cors.rb`
103
+
104
+ Wrapper around the battle-tested `rack-cors` gem.
105
+
106
+ ```ruby
107
+ module FunApi
108
+ module Middleware
109
+ class Cors
110
+ def self.new(app, allow_origins: ['*'], allow_methods: ['*'],
111
+ allow_headers: ['*'], expose_headers: [],
112
+ max_age: 600, allow_credentials: false)
113
+ require 'rack/cors'
114
+
115
+ Rack::Cors.new(app) do |config|
116
+ config.allow do |allow|
117
+ allow.origins(*allow_origins)
118
+ allow.resource '*',
119
+ methods: allow_methods,
120
+ headers: allow_headers,
121
+ expose: expose_headers,
122
+ max_age: max_age,
123
+ credentials: allow_credentials
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ class App
131
+ def add_cors(**options)
132
+ use FunApi::Middleware::Cors, **options
133
+ end
134
+ end
135
+ end
136
+ ```
137
+
138
+ **Dependencies**: Add `rack-cors` to gemspec
139
+
140
+ #### C. Trusted Host Middleware
141
+ **File**: `lib/fun_api/middleware/trusted_host.rb`
142
+
143
+ Validates the `Host` header to prevent host header attacks.
144
+
145
+ ```ruby
146
+ module FunApi
147
+ module Middleware
148
+ class TrustedHost < Base
149
+ def initialize(app, allowed_hosts: [])
150
+ super(app)
151
+ @allowed_hosts = Array(allowed_hosts)
152
+ end
153
+
154
+ def call(env)
155
+ host = env['HTTP_HOST']&.split(':')&.first
156
+
157
+ unless host_allowed?(host)
158
+ return [
159
+ 400,
160
+ {'content-type' => 'application/json'},
161
+ [JSON.dump(detail: 'Invalid host header')]
162
+ ]
163
+ end
164
+
165
+ @app.call(env)
166
+ end
167
+
168
+ private
169
+
170
+ def host_allowed?(host)
171
+ return true if @allowed_hosts.empty?
172
+ @allowed_hosts.any? { |pattern|
173
+ pattern.is_a?(Regexp) ? pattern.match?(host) : pattern == host
174
+ }
175
+ end
176
+ end
177
+ end
178
+
179
+ class App
180
+ def add_trusted_host(allowed_hosts:)
181
+ use FunApi::Middleware::TrustedHost, allowed_hosts: allowed_hosts
182
+ end
183
+ end
184
+ end
185
+ ```
186
+
187
+ #### D. Request Logger Middleware
188
+ **File**: `lib/fun_api/middleware/request_logger.rb`
189
+
190
+ Logs incoming requests with timing information.
191
+
192
+ ```ruby
193
+ module FunApi
194
+ module Middleware
195
+ class RequestLogger < Base
196
+ def initialize(app, logger: nil, level: :info)
197
+ super(app)
198
+ @logger = logger || Logger.new($stdout)
199
+ @level = level
200
+ end
201
+
202
+ def call(env)
203
+ start = Time.now
204
+ status, headers, body = @app.call(env)
205
+ duration = Time.now - start
206
+
207
+ log_request(env, status, duration)
208
+
209
+ [status, headers, body]
210
+ end
211
+
212
+ private
213
+
214
+ def log_request(env, status, duration)
215
+ request = Rack::Request.new(env)
216
+ @logger.send(@level,
217
+ "#{request.request_method} #{request.path} " \
218
+ "#{status} #{(duration * 1000).round(2)}ms"
219
+ )
220
+ end
221
+ end
222
+ end
223
+
224
+ class App
225
+ def add_request_logger(logger: nil, level: :info)
226
+ use FunApi::Middleware::RequestLogger, logger: logger, level: level
227
+ end
228
+ end
229
+ end
230
+ ```
231
+
232
+ #### E. Gzip Compression (Delegate to Rack::Deflater)
233
+ **File**: Convenience method only
234
+
235
+ ```ruby
236
+ class App
237
+ def add_gzip
238
+ use Rack::Deflater, if: ->(env, status, headers, body) {
239
+ headers['content-type']&.start_with?('application/json')
240
+ }
241
+ end
242
+ end
243
+ ```
244
+
245
+ ### Phase 3: Documentation & Examples ⭐⭐⭐
246
+
247
+ #### Update README.md
248
+ Add middleware section with examples:
249
+ - Basic middleware usage
250
+ - Built-in middleware
251
+ - Compatible Rack middleware ecosystem
252
+ - Custom middleware creation
253
+
254
+ #### Create Example App
255
+ **File**: `examples/middleware_demo.rb`
256
+
257
+ Demonstrate all middleware features.
258
+
259
+ ### Phase 4: Testing ⭐⭐⭐⭐
260
+
261
+ **File**: `test/test_middleware.rb`
262
+
263
+ Test:
264
+ - Middleware ordering (LIFO)
265
+ - Built-in middleware functionality
266
+ - Async compatibility
267
+ - Integration with Rack middleware
268
+ - Edge cases (no middleware, single middleware, multiple middleware)
269
+
270
+ ## Benefits
271
+
272
+ 1. ✅ **Leverage Ruby ecosystem** - All existing Rack middleware works out of the box
273
+ 2. ✅ **Battle-tested** - Rack middleware pattern is 15+ years proven
274
+ 3. ✅ **FastAPI-like DX** - Convenience methods (`add_cors`, `add_gzip`) for common needs
275
+ 4. ✅ **Flexibility** - Support both Rack standard and custom middleware
276
+ 5. ✅ **Async-compatible** - Rack 3.0+ supports async natively
277
+ 6. ✅ **Zero reinvention** - Delegate to proven gems (rack-cors, rack-deflater)
278
+
279
+ ## Compatible Rack Middleware (to document)
280
+
281
+ Popular Rack middleware that works immediately:
282
+
283
+ ```ruby
284
+ # Rate limiting
285
+ gem 'rack-attack'
286
+ app.use Rack::Attack
287
+
288
+ # Authentication
289
+ gem 'warden'
290
+ app.use Warden::Manager
291
+
292
+ # Caching
293
+ app.use Rack::Cache
294
+
295
+ # ETags
296
+ app.use Rack::ETag
297
+
298
+ # Conditional GET
299
+ app.use Rack::ConditionalGet
300
+
301
+ # Static files
302
+ app.use Rack::Static, urls: ['/public']
303
+
304
+ # Session
305
+ app.use Rack::Session::Cookie, secret: 'your_secret'
306
+ ```
307
+
308
+ ## Implementation Checklist
309
+
310
+ ### Phase 1: Core System
311
+ - [ ] Add `use` method to `App` class
312
+ - [ ] Implement `build_middleware_chain` method
313
+ - [ ] Update `call` method to use middleware chain
314
+ - [ ] Test with existing Rack middleware (Rack::Cors)
315
+ - [ ] Ensure middleware ordering is correct (LIFO)
316
+
317
+ ### Phase 2: Built-in Middleware
318
+ - [ ] Create `lib/fun_api/middleware/base.rb`
319
+ - [ ] Create `lib/fun_api/middleware/cors.rb` (wrapper)
320
+ - [ ] Create `lib/fun_api/middleware/trusted_host.rb`
321
+ - [ ] Create `lib/fun_api/middleware/request_logger.rb`
322
+ - [ ] Add convenience methods: `add_cors`, `add_gzip`, etc.
323
+ - [ ] Add `rack-cors` to gemspec dependencies
324
+
325
+ ### Phase 3: Documentation
326
+ - [ ] Update README with middleware section
327
+ - [ ] Document built-in middleware
328
+ - [ ] List compatible Rack middleware
329
+ - [ ] Create middleware guide
330
+ - [ ] Add examples for common use cases (auth, CORS, logging)
331
+ - [ ] Create `examples/middleware_demo.rb`
332
+
333
+ ### Phase 4: Testing
334
+ - [ ] Create `test/test_middleware.rb`
335
+ - [ ] Test middleware ordering
336
+ - [ ] Test async compatibility
337
+ - [ ] Test with popular Rack middleware
338
+ - [ ] Integration tests for built-in middleware
339
+ - [ ] Edge case testing
340
+
341
+ ## Questions Resolved
342
+
343
+ **Q: Should we leverage Rack for middleware?**
344
+ A: YES! Rack middleware is battle-tested and gives us access to the entire Ruby ecosystem. We should support standard Rack middleware while providing FastAPI-style convenience wrappers.
345
+
346
+ **Q: Which middleware should be built-in vs ecosystem?**
347
+ A:
348
+ - **Built-in**: CORS (wrapped), TrustedHost, RequestLogger (most common needs)
349
+ - **Ecosystem**: Rate limiting (rack-attack), Auth (warden), Caching (rack-cache)
350
+ - **Delegate to Rack**: Gzip (Rack::Deflater), ETag (Rack::ETag)
351
+
352
+ **Q: How to maintain async compatibility?**
353
+ A: Rack 3.0+ already supports async via Fiber/Async. Our middleware just needs to follow standard Rack interface (`call(env)` returns `[status, headers, body]`).
@@ -0,0 +1,325 @@
1
+ # Background Tasks Analysis for FunAPI
2
+
3
+ ## Executive Summary
4
+
5
+ After extensive testing of Ruby's Async gem, **we can implement background tasks with ZERO abstraction** - users can simply use `Async(task) do ... end` to spawn fire-and-forget tasks. However, there's a critical **dependency cleanup issue** that makes a lightweight `BackgroundTasks` abstraction valuable.
6
+
7
+ ## Key Findings
8
+
9
+ ### Python vs Ruby: Task Lifecycle
10
+
11
+ **Python (asyncio):**
12
+ - `asyncio.create_task()` returns immediately without waiting
13
+ - Tasks can be "orphaned" if not explicitly awaited
14
+ - This is WHY FastAPI needs `BackgroundTasks` - to ensure tasks complete before process exits
15
+
16
+ **Ruby (Async gem):**
17
+ - `task.async { }` creates a child task that IS tracked by parent
18
+ - Parent waits for children when block ends (via `Async do ... end.wait`)
19
+ - `Async(parent) { }` creates a DETACHED task (truly fire-and-forget)
20
+
21
+ ### The Critical Problem: Dependency Cleanup
22
+
23
+ ```ruby
24
+ Async do |task|
25
+ begin
26
+ db = Database.connect
27
+
28
+ # User spawns background task
29
+ Async(task) do
30
+ sleep 0.1
31
+ send_email(db) # DB is captured in closure
32
+ end
33
+
34
+ [response, 200]
35
+
36
+ ensure
37
+ db.close # RUNS BEFORE background task!
38
+ end
39
+ end
40
+ ```
41
+
42
+ **Timeline:**
43
+ 1. Request handler starts
44
+ 2. Response returned
45
+ 3. **ensure block runs → DB closed**
46
+ 4. Background task tries to use DB (might work due to closure, but semantically wrong)
47
+
48
+ ### Solution Options
49
+
50
+ #### Option 1: No Abstraction (Pure Async)
51
+
52
+ ```ruby
53
+ api.post '/notify' do |input, req, task|
54
+ email = input[:body][:email]
55
+
56
+ # Detached background task
57
+ Async(task) do
58
+ send_welcome_email(email)
59
+ end
60
+
61
+ [{ message: "Sent" }, 200]
62
+ end
63
+ ```
64
+
65
+ **Pros:**
66
+ - Zero magic, pure Ruby Async
67
+ - No new API to learn
68
+ - Explicit and clear
69
+
70
+ **Cons:**
71
+ - ❌ Runs AFTER dependency cleanup
72
+ - ❌ Dependencies might be closed when task runs
73
+ - ❌ No FastAPI parity
74
+ - ❌ No task collection/management
75
+
76
+ #### Option 2: BackgroundTasks Object (Recommended)
77
+
78
+ ```ruby
79
+ api.post '/notify' do |input, req, task, background:|
80
+ email = input[:body][:email]
81
+
82
+ background.add_task(:send_welcome_email, email)
83
+
84
+ [{ message: "Sent" }, 200]
85
+ end
86
+ ```
87
+
88
+ **Implementation ensures correct execution order:**
89
+ 1. Handler returns response tuple
90
+ 2. **Background tasks execute** (with dependencies still available)
91
+ 3. Dependency cleanup in ensure block
92
+ 4. Response sent to client
93
+
94
+ **Pros:**
95
+ - ✅ Tasks run BEFORE dependency cleanup
96
+ - ✅ FastAPI API parity
97
+ - ✅ Dependencies guaranteed available
98
+ - ✅ Can inject dependencies into background tasks
99
+ - ✅ Error handling for task failures
100
+ - ✅ Testing/introspection support
101
+
102
+ **Cons:**
103
+ - Small abstraction needed (~50 lines)
104
+
105
+ ## Recommended Implementation
106
+
107
+ ### BackgroundTasks Class
108
+
109
+ ```ruby
110
+ module FunApi
111
+ class BackgroundTasks
112
+ def initialize(task, context)
113
+ @task = task
114
+ @context = context
115
+ @tasks = []
116
+ end
117
+
118
+ def add_task(callable, *args, **kwargs)
119
+ @tasks << { callable: callable, args: args, kwargs: kwargs }
120
+ end
121
+
122
+ def execute
123
+ @tasks.each do |task_def|
124
+ callable = task_def[:callable]
125
+ args = task_def[:args]
126
+ kwargs = task_def[:kwargs]
127
+
128
+ # Spawn as child task (not detached)
129
+ @task.async do
130
+ if callable.respond_to?(:call)
131
+ callable.call(*args, **kwargs)
132
+ elsif callable.is_a?(Symbol) && @context.respond_to?(callable)
133
+ @context.public_send(callable, *args, **kwargs)
134
+ end
135
+ rescue => e
136
+ warn "Background task failed: #{e.message}"
137
+ end
138
+ end
139
+
140
+ # Wait for all background tasks to complete
141
+ @task.children.each(&:wait)
142
+ end
143
+ end
144
+ end
145
+ ```
146
+
147
+ ### Modified Request Flow
148
+
149
+ ```ruby
150
+ def handle_async_route(req, path_params, body_schema, query_schema, response_schema, dependencies, &blk)
151
+ current_task = Async::Task.current
152
+ cleanup_objects = []
153
+ background_tasks = BackgroundTasks.new(current_task, self)
154
+
155
+ begin
156
+ # ... input validation ...
157
+ # ... dependency resolution ...
158
+
159
+ resolved_deps[:background] = background_tasks
160
+
161
+ payload, status = blk.call(input, req, current_task, **resolved_deps)
162
+
163
+ # Execute background tasks BEFORE ensure
164
+ background_tasks.execute
165
+
166
+ # ... response building ...
167
+
168
+ ensure
169
+ cleanup_objects.each(&:cleanup)
170
+ end
171
+ end
172
+ ```
173
+
174
+ ### Execution Order
175
+
176
+ ```
177
+ 1. Request arrives
178
+ 2. Dependencies resolved (DB connect, etc.)
179
+ 3. Route handler executes
180
+ 4. Handler returns [payload, status]
181
+ 5. ⭐ Background tasks execute (deps still available)
182
+ 6. Background tasks complete
183
+ 7. Dependencies cleaned up (DB close, etc.)
184
+ 8. Response tuple returned
185
+ 9. HTTP layer sends response to client
186
+ ```
187
+
188
+ ## Use Cases
189
+
190
+ ### Perfect For:
191
+ - Email notifications after signup
192
+ - Logging/analytics after request
193
+ - Cache warming
194
+ - Simple webhook calls
195
+ - Audit trail recording
196
+
197
+ ### NOT For:
198
+ - Long-running jobs (> 30 seconds)
199
+ - Jobs requiring persistence/retries
200
+ - Jobs that must survive server restart
201
+ - Distributed job processing
202
+ → Use Sidekiq, GoodJob, or Que instead
203
+
204
+ ## API Design
205
+
206
+ ### As Dependency (Recommended)
207
+
208
+ ```ruby
209
+ api.post '/signup', body: UserSchema do |input, req, task, background:|
210
+ user = create_user(input[:body])
211
+
212
+ background.add_task(:send_welcome_email, user[:email])
213
+ background.add_task(:notify_admin, user)
214
+
215
+ [user, 201]
216
+ end
217
+ ```
218
+
219
+ ### With Callable Objects
220
+
221
+ ```ruby
222
+ background.add_task(method(:send_email), to: user[:email])
223
+ background.add_task(lambda { |id| log_event(id) }, user[:id])
224
+ ```
225
+
226
+ ### With Dependencies
227
+
228
+ ```ruby
229
+ api.register(:mailer) { Mailer.new }
230
+
231
+ api.post '/signup', depends: [:mailer] do |input, req, task, mailer:, background:|
232
+ user = create_user(input[:body])
233
+
234
+ # Background task can use mailer (captured in closure)
235
+ background.add_task(lambda { mailer.send_welcome(user[:email]) })
236
+
237
+ [user, 201]
238
+ end
239
+ ```
240
+
241
+ ## Comparison to FastAPI
242
+
243
+ ### FastAPI
244
+ ```python
245
+ from fastapi import BackgroundTasks
246
+
247
+ @app.post("/send-notification/{email}")
248
+ async def notify(email: str, background_tasks: BackgroundTasks):
249
+ background_tasks.add_task(send_email, email, message="Hi")
250
+ return {"message": "Sent"}
251
+ ```
252
+
253
+ ### FunAPI
254
+ ```ruby
255
+ api.post '/send-notification/:email' do |input, req, task, background:|
256
+ email = input[:path]['email']
257
+ background.add_task(:send_email, email, message: "Hi")
258
+ [{ message: "Sent" }, 200]
259
+ end
260
+ ```
261
+
262
+ **Nearly identical!** ✨
263
+
264
+ ## Testing Strategy
265
+
266
+ ```ruby
267
+ class TestBackgroundTasks < Minitest::Test
268
+ def test_background_tasks_execute_after_response
269
+ execution_order = []
270
+
271
+ app = FunApi::App.new do |api|
272
+ api.post '/test' do |input, req, task, background:|
273
+ execution_order << :handler
274
+
275
+ background.add_task(lambda { execution_order << :background })
276
+
277
+ [{ ok: true }, 200]
278
+ end
279
+ end
280
+
281
+ res = async_request(app, :post, '/test')
282
+
283
+ assert_equal [:handler, :background], execution_order
284
+ assert_equal 200, res.status
285
+ end
286
+
287
+ def test_background_tasks_can_access_dependencies
288
+ # Test that DB is still available in background task
289
+ end
290
+
291
+ def test_background_task_errors_are_handled
292
+ # Test that task errors don't crash app
293
+ end
294
+ end
295
+ ```
296
+
297
+ ## Recommendation: Implement BackgroundTasks
298
+
299
+ **Why:**
300
+ 1. ✅ Correct execution order (before dependency cleanup)
301
+ 2. ✅ FastAPI API parity
302
+ 3. ✅ Safe dependency access
303
+ 4. ✅ Better error handling
304
+ 5. ✅ Testable and introspectable
305
+ 6. ✅ Small implementation (~50-80 lines)
306
+
307
+ **Why not "no API":**
308
+ 1. ❌ `Async(task) { }` runs AFTER cleanup
309
+ 2. ❌ Dependencies might be closed
310
+ 3. ❌ No error handling
311
+ 4. ❌ Harder to test
312
+ 5. ❌ Less discoverable
313
+
314
+ ## Next Steps
315
+
316
+ 1. Implement `BackgroundTasks` class
317
+ 2. Modify `handle_async_route` to inject and execute
318
+ 3. Write comprehensive tests
319
+ 4. Create examples (email, logging, webhooks)
320
+ 5. Update docs (README, AGENTS.md)
321
+ 6. Consider dependency injection into background tasks
322
+
323
+ **Estimated effort:** 2-3 hours
324
+ **Impact:** High - production-critical feature
325
+ **Complexity:** Low - leverages existing Async infrastructure