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,127 @@
1
+ ---
2
+ title: Quick Start
3
+ ---
4
+
5
+ # Quick Start
6
+
7
+ Get a FunApi application running in under 5 minutes.
8
+
9
+ ## Installation
10
+
11
+ Add FunApi to your Gemfile:
12
+
13
+ ```ruby
14
+ gem 'funapi'
15
+ ```
16
+
17
+ Then install:
18
+
19
+ ```bash
20
+ bundle install
21
+ ```
22
+
23
+ ## Create Your First App
24
+
25
+ Create a file called `app.rb`:
26
+
27
+ ```ruby
28
+ require 'funapi'
29
+ require 'funapi/server/falcon'
30
+
31
+ app = FunApi::App.new(
32
+ title: "My First API",
33
+ version: "1.0.0"
34
+ ) do |api|
35
+ api.get '/hello' do |input, req, task|
36
+ [{ message: 'Hello, World!' }, 200]
37
+ end
38
+
39
+ api.get '/hello/:name' do |input, req, task|
40
+ name = input[:path]['name']
41
+ [{ message: "Hello, #{name}!" }, 200]
42
+ end
43
+ end
44
+
45
+ FunApi::Server::Falcon.start(app, port: 3000)
46
+ ```
47
+
48
+ ## Run It
49
+
50
+ ```bash
51
+ ruby app.rb
52
+ ```
53
+
54
+ You should see:
55
+
56
+ ```
57
+ Falcon listening on 0.0.0.0:3000
58
+ Try: curl http://0.0.0.0:3000/hello
59
+ Press Ctrl+C to stop
60
+ ```
61
+
62
+ ## Test It
63
+
64
+ ```bash
65
+ $ curl http://localhost:3000/hello
66
+ {"message":"Hello, World!"}
67
+
68
+ $ curl http://localhost:3000/hello/Ruby
69
+ {"message":"Hello, Ruby!"}
70
+ ```
71
+
72
+ ## Check the Docs
73
+
74
+ Open your browser to `http://localhost:3000/docs`
75
+
76
+ You'll see interactive Swagger UI documentation automatically generated from your routes.
77
+
78
+ ## Add Validation
79
+
80
+ Let's add a POST endpoint with request validation:
81
+
82
+ ```ruby
83
+ require 'funapi'
84
+ require 'funapi/server/falcon'
85
+
86
+ UserSchema = FunApi::Schema.define do
87
+ required(:name).filled(:string)
88
+ required(:email).filled(:string)
89
+ optional(:age).filled(:integer)
90
+ end
91
+
92
+ app = FunApi::App.new(title: "My API") do |api|
93
+ api.get '/hello' do |input, req, task|
94
+ [{ message: 'Hello, World!' }, 200]
95
+ end
96
+
97
+ api.post '/users', body: UserSchema do |input, req, task|
98
+ user = input[:body]
99
+ # user is already validated!
100
+ [{ created: user }, 201]
101
+ end
102
+ end
103
+
104
+ FunApi::Server::Falcon.start(app, port: 3000)
105
+ ```
106
+
107
+ Test the validation:
108
+
109
+ ```bash
110
+ # Valid request
111
+ $ curl -X POST http://localhost:3000/users \
112
+ -H 'Content-Type: application/json' \
113
+ -d '{"name":"Alice","email":"alice@example.com"}'
114
+ {"created":{"name":"Alice","email":"alice@example.com"}}
115
+
116
+ # Invalid request (missing email)
117
+ $ curl -X POST http://localhost:3000/users \
118
+ -H 'Content-Type: application/json' \
119
+ -d '{"name":"Alice"}'
120
+ {"detail":[{"loc":["body","email"],"msg":"is missing","type":"value_error"}]}
121
+ ```
122
+
123
+ ## Next Steps
124
+
125
+ - [Key Concepts](/docs/getting-started/key-concepts) - Understand the core ideas
126
+ - [Routing](/docs/essential/routing) - Learn about path parameters and HTTP methods
127
+ - [Validation](/docs/essential/validation) - Deep dive into dry-schema
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: FunApi
3
+ ---
4
+
5
+ # FunApi
6
+
7
+ A minimal, async-first Ruby web framework inspired by FastAPI.
8
+
9
+ ```ruby
10
+ require 'funapi'
11
+ require 'funapi/server/falcon'
12
+
13
+ UserSchema = FunApi::Schema.define do
14
+ required(:name).filled(:string)
15
+ required(:email).filled(:string)
16
+ end
17
+
18
+ app = FunApi::App.new(title: "My API", version: "1.0.0") do |api|
19
+ api.get '/hello' do |input, req, task|
20
+ [{ message: 'Hello, World!' }, 200]
21
+ end
22
+
23
+ api.post '/users', body: UserSchema do |input, req, task|
24
+ [{ created: input[:body] }, 201]
25
+ end
26
+ end
27
+
28
+ FunApi::Server::Falcon.start(app, port: 3000)
29
+ ```
30
+
31
+ Visit `http://localhost:3000/docs` to see your interactive API documentation.
32
+
33
+ ## Key Features
34
+
35
+ - **Async-first** - Built on Falcon and Ruby's Async library for high-performance concurrent operations
36
+ - **Simple validation** - Using dry-schema for straightforward request/response validation
37
+ - **Auto-documentation** - Automatic OpenAPI/Swagger docs generated from your code
38
+ - **Minimal magic** - Clear, explicit APIs without heavy DSLs
39
+ - **Rack-compatible** - Works with any Rack middleware
40
+
41
+ ## Standing on Giants
42
+
43
+ FunApi brings together proven Ruby libraries:
44
+
45
+ - **[Falcon](https://github.com/socketry/falcon)** - High-performance async HTTP server
46
+ - **[dry-schema](https://dry-rb.org/gems/dry-schema/)** - Powerful, composable validation
47
+
48
+ ## Installation
49
+
50
+ Add to your Gemfile:
51
+
52
+ ```ruby
53
+ gem 'funapi'
54
+ ```
55
+
56
+ Then run:
57
+
58
+ ```bash
59
+ bundle install
60
+ ```
61
+
62
+ ## Quick Example
63
+
64
+ ```ruby
65
+ require 'funapi'
66
+ require 'funapi/server/falcon'
67
+
68
+ app = FunApi::App.new do |api|
69
+ api.get '/hello/:name' do |input, req, task|
70
+ name = input[:path]['name']
71
+ [{ message: "Hello, #{name}!" }, 200]
72
+ end
73
+ end
74
+
75
+ FunApi::Server::Falcon.start(app, port: 3000)
76
+ ```
77
+
78
+ ```bash
79
+ $ curl http://localhost:3000/hello/world
80
+ {"message":"Hello, world!"}
81
+ ```
@@ -0,0 +1,137 @@
1
+ ---
2
+ title: Async Operations
3
+ ---
4
+
5
+ # Async Operations
6
+
7
+ FunApi is async-first. Every handler receives an `Async::Task` for concurrent operations.
8
+
9
+ ## The Task Parameter
10
+
11
+ The third handler parameter is an `Async::Task`:
12
+
13
+ ```ruby
14
+ api.get '/dashboard' do |input, req, task|
15
+ # task is Async::Task.current
16
+ end
17
+ ```
18
+
19
+ ## Concurrent Fetches
20
+
21
+ Run multiple operations in parallel:
22
+
23
+ ```ruby
24
+ api.get '/dashboard/:id' do |input, req, task|
25
+ id = input[:path]['id']
26
+
27
+ # These run concurrently
28
+ user_task = task.async { fetch_user(id) }
29
+ posts_task = task.async { fetch_posts(id) }
30
+ stats_task = task.async { fetch_stats(id) }
31
+
32
+ # Wait for all to complete
33
+ [{
34
+ user: user_task.wait,
35
+ posts: posts_task.wait,
36
+ stats: stats_task.wait
37
+ }, 200]
38
+ end
39
+ ```
40
+
41
+ Without async, this would take `time(user) + time(posts) + time(stats)`.
42
+ With async, it takes `max(time(user), time(posts), time(stats))`.
43
+
44
+ ## Error Handling
45
+
46
+ Handle errors from async operations:
47
+
48
+ ```ruby
49
+ api.get '/data' do |input, req, task|
50
+ primary = task.async { fetch_from_primary }
51
+ fallback = task.async { fetch_from_fallback }
52
+
53
+ begin
54
+ [{ data: primary.wait }, 200]
55
+ rescue => e
56
+ # Primary failed, use fallback
57
+ [{ data: fallback.wait, source: 'fallback' }, 200]
58
+ end
59
+ end
60
+ ```
61
+
62
+ ## Timeouts
63
+
64
+ Add timeouts to operations:
65
+
66
+ ```ruby
67
+ api.get '/external' do |input, req, task|
68
+ result = task.with_timeout(5) do
69
+ fetch_from_slow_api
70
+ end
71
+
72
+ [{ data: result }, 200]
73
+ rescue Async::TimeoutError
74
+ raise FunApi::HTTPException.new(
75
+ status_code: 504,
76
+ detail: "External API timeout"
77
+ )
78
+ end
79
+ ```
80
+
81
+ ## Sleep
82
+
83
+ Use `Kernel#sleep` (not `task.sleep`):
84
+
85
+ ```ruby
86
+ api.get '/delayed' do |input, req, task|
87
+ sleep(1) # Non-blocking in async context
88
+ [{ message: 'Done' }, 200]
89
+ end
90
+ ```
91
+
92
+ ## Real-World Example
93
+
94
+ ```ruby
95
+ api.get '/user/:id/feed' do |input, req, task|
96
+ user_id = input[:path]['id']
97
+
98
+ # Fetch user and check permissions first
99
+ user = fetch_user(user_id)
100
+ raise FunApi::HTTPException.new(status_code: 404) unless user
101
+
102
+ # Then fetch feed data concurrently
103
+ posts = task.async { Post.where(user_id: user_id).limit(20) }
104
+ notifications = task.async { Notification.unread(user_id) }
105
+ suggestions = task.async { RecommendationService.for(user_id) }
106
+
107
+ [{
108
+ user: user,
109
+ posts: posts.wait,
110
+ notifications: notifications.wait,
111
+ suggestions: suggestions.wait
112
+ }, 200]
113
+ end
114
+ ```
115
+
116
+ ## When to Use Async
117
+
118
+ **Good candidates:**
119
+ - Multiple independent database queries
120
+ - External API calls
121
+ - File I/O operations
122
+ - Any I/O-bound work
123
+
124
+ **Not needed for:**
125
+ - CPU-bound calculations
126
+ - Single database query
127
+ - Simple transformations
128
+
129
+ ## Technical Details
130
+
131
+ FunApi uses Ruby's [Async](https://github.com/socketry/async) library and [Falcon](https://github.com/socketry/falcon) server. The task parameter is the current `Async::Task`, giving you access to the full Async API.
132
+
133
+ ```ruby
134
+ # These are equivalent
135
+ task.async { work }
136
+ Async::Task.current.async { work }
137
+ ```
@@ -0,0 +1,143 @@
1
+ ---
2
+ title: Background Tasks
3
+ ---
4
+
5
+ # Background Tasks
6
+
7
+ Execute tasks after the response is sent to the client.
8
+
9
+ ## Basic Usage
10
+
11
+ Request the `background:` parameter in your handler:
12
+
13
+ ```ruby
14
+ api.post '/signup', body: UserSchema do |input, req, task, background:|
15
+ user = create_user(input[:body])
16
+
17
+ # These run AFTER the response is sent
18
+ background.add_task(method(:send_welcome_email), user[:email])
19
+ background.add_task(method(:log_signup), user[:id])
20
+
21
+ [{ user: user }, 201]
22
+ end
23
+ ```
24
+
25
+ The client receives the response immediately. The tasks execute afterward.
26
+
27
+ ## Adding Tasks
28
+
29
+ ### With Method References
30
+
31
+ ```ruby
32
+ def send_email(to, subject)
33
+ # send email
34
+ end
35
+
36
+ background.add_task(method(:send_email), "user@example.com", "Welcome!")
37
+ ```
38
+
39
+ ### With Lambdas
40
+
41
+ ```ruby
42
+ background.add_task(->(email) { Mailer.send(email) }, user[:email])
43
+ ```
44
+
45
+ ### With Keyword Arguments
46
+
47
+ ```ruby
48
+ background.add_task(
49
+ ->(to:, subject:) { send_email(to: to, subject: subject) },
50
+ to: "user@example.com",
51
+ subject: "Welcome!"
52
+ )
53
+ ```
54
+
55
+ ## With Dependencies
56
+
57
+ Dependencies are available to background tasks:
58
+
59
+ ```ruby
60
+ api.register(:mailer) { Mailer.new }
61
+ api.register(:analytics) { Analytics.new }
62
+
63
+ api.post '/signup', depends: [:mailer, :analytics] do |input, req, task, mailer:, analytics:, background:|
64
+ user = create_user(input[:body])
65
+
66
+ # Dependencies captured in closure
67
+ background.add_task(lambda {
68
+ mailer.send_welcome(user[:email])
69
+ analytics.track('signup', user[:id])
70
+ })
71
+
72
+ [{ user: user }, 201]
73
+ end
74
+ ```
75
+
76
+ ## Error Handling
77
+
78
+ Background task errors are logged but don't affect the response:
79
+
80
+ ```ruby
81
+ background.add_task(lambda {
82
+ raise "Task failed!"
83
+ # Logged as warning, doesn't crash the server
84
+ })
85
+ ```
86
+
87
+ ## When to Use
88
+
89
+ **Good for:**
90
+ - Email notifications
91
+ - Logging and analytics
92
+ - Cache warming
93
+ - Simple webhook calls
94
+ - Audit trail recording
95
+
96
+ **Not for:**
97
+ - Long-running jobs (> 30 seconds)
98
+ - Jobs requiring persistence
99
+ - Jobs that must survive restarts
100
+ - Jobs needing retries
101
+
102
+ For complex jobs, use a proper job queue like Sidekiq or GoodJob.
103
+
104
+ ## Complete Example
105
+
106
+ ```ruby
107
+ require 'funapi'
108
+ require 'funapi/server/falcon'
109
+
110
+ def send_welcome_email(email)
111
+ puts "Sending welcome email to #{email}"
112
+ # Actually send email
113
+ end
114
+
115
+ def log_signup(user_id)
116
+ puts "Logging signup for user #{user_id}"
117
+ # Log to analytics
118
+ end
119
+
120
+ def notify_admin(user)
121
+ puts "Notifying admin about new user: #{user[:name]}"
122
+ # Send Slack notification
123
+ end
124
+
125
+ app = FunApi::App.new do |api|
126
+ UserSchema = FunApi::Schema.define do
127
+ required(:name).filled(:string)
128
+ required(:email).filled(:string)
129
+ end
130
+
131
+ api.post '/signup', body: UserSchema do |input, req, task, background:|
132
+ user = { id: rand(1000), **input[:body] }
133
+
134
+ background.add_task(method(:send_welcome_email), user[:email])
135
+ background.add_task(method(:log_signup), user[:id])
136
+ background.add_task(method(:notify_admin), user)
137
+
138
+ [{ user: user, message: 'Check your email!' }, 201]
139
+ end
140
+ end
141
+
142
+ FunApi::Server::Falcon.start(app, port: 3000)
143
+ ```
@@ -0,0 +1,175 @@
1
+ ---
2
+ title: Database
3
+ ---
4
+
5
+ # Database
6
+
7
+ FunApi works with any database library. Here's how to integrate common options.
8
+
9
+ ## With db-postgres
10
+
11
+ [db-postgres](https://github.com/socketry/db-postgres) is async-native and works great with FunApi:
12
+
13
+ ```ruby
14
+ require 'funapi'
15
+ require 'db/postgres'
16
+ require 'funapi/server/falcon'
17
+
18
+ app = FunApi::App.new do |api|
19
+ api.on_startup do
20
+ $db = DB::Postgres::Connection.new(
21
+ host: 'localhost',
22
+ database: 'myapp',
23
+ user: 'postgres'
24
+ )
25
+ end
26
+
27
+ api.on_shutdown do
28
+ $db&.close
29
+ end
30
+
31
+ api.get '/users' do |input, req, task|
32
+ result = $db.query("SELECT id, name, email FROM users")
33
+ [{ users: result.to_a }, 200]
34
+ end
35
+ end
36
+ ```
37
+
38
+ ## With Sequel
39
+
40
+ [Sequel](https://sequel.jeremyevans.net/) is a powerful database toolkit:
41
+
42
+ ```ruby
43
+ require 'funapi'
44
+ require 'sequel'
45
+ require 'funapi/server/falcon'
46
+
47
+ DB = Sequel.connect(ENV['DATABASE_URL'])
48
+
49
+ app = FunApi::App.new do |api|
50
+ api.get '/users' do |input, req, task|
51
+ users = DB[:users].all
52
+ [{ users: users }, 200]
53
+ end
54
+
55
+ api.get '/users/:id' do |input, req, task|
56
+ user = DB[:users].where(id: input[:path]['id']).first
57
+ raise FunApi::HTTPException.new(status_code: 404) unless user
58
+ [{ user: user }, 200]
59
+ end
60
+
61
+ api.post '/users', body: UserSchema do |input, req, task|
62
+ id = DB[:users].insert(input[:body])
63
+ user = DB[:users].where(id: id).first
64
+ [{ user: user }, 201]
65
+ end
66
+ end
67
+ ```
68
+
69
+ ## With ActiveRecord
70
+
71
+ You can use ActiveRecord standalone (without Rails):
72
+
73
+ ```ruby
74
+ require 'funapi'
75
+ require 'active_record'
76
+ require 'funapi/server/falcon'
77
+
78
+ ActiveRecord::Base.establish_connection(ENV['DATABASE_URL'])
79
+
80
+ class User < ActiveRecord::Base
81
+ end
82
+
83
+ app = FunApi::App.new do |api|
84
+ api.get '/users' do |input, req, task|
85
+ users = User.all.map(&:attributes)
86
+ [{ users: users }, 200]
87
+ end
88
+
89
+ api.post '/users', body: UserSchema do |input, req, task|
90
+ user = User.create!(input[:body])
91
+ [{ user: user.attributes }, 201]
92
+ end
93
+ end
94
+ ```
95
+
96
+ ## Connection Pooling with Dependencies
97
+
98
+ Use dependency injection for connection management:
99
+
100
+ ```ruby
101
+ require 'connection_pool'
102
+
103
+ app = FunApi::App.new do |api|
104
+ pool = ConnectionPool.new(size: 10) do
105
+ PG.connect(ENV['DATABASE_URL'])
106
+ end
107
+
108
+ api.register(:db) do
109
+ conn = pool.checkout
110
+ [conn, -> { pool.checkin(conn) }]
111
+ end
112
+
113
+ api.get '/users', depends: [:db] do |input, req, task, db:|
114
+ result = db.exec("SELECT * FROM users")
115
+ [{ users: result.to_a }, 200]
116
+ end
117
+ end
118
+ ```
119
+
120
+ ## Transactions
121
+
122
+ ### With Block Dependencies
123
+
124
+ ```ruby
125
+ api.register(:transaction) do |yielder|
126
+ conn = PG.connect(ENV['DATABASE_URL'])
127
+ conn.exec("BEGIN")
128
+
129
+ yielder.call(conn)
130
+
131
+ conn.exec("COMMIT")
132
+ rescue
133
+ conn.exec("ROLLBACK")
134
+ raise
135
+ ensure
136
+ conn.close
137
+ end
138
+
139
+ api.post '/transfer', depends: [:transaction] do |input, req, task, transaction:|
140
+ transaction.exec("UPDATE accounts SET balance = balance - $1 WHERE id = $2",
141
+ [input[:body][:amount], input[:body][:from_id]])
142
+ transaction.exec("UPDATE accounts SET balance = balance + $1 WHERE id = $2",
143
+ [input[:body][:amount], input[:body][:to_id]])
144
+
145
+ [{ success: true }, 200]
146
+ end
147
+ ```
148
+
149
+ ## Async Queries
150
+
151
+ With async-compatible drivers, run queries concurrently:
152
+
153
+ ```ruby
154
+ api.get '/dashboard/:id' do |input, req, task|
155
+ id = input[:path]['id']
156
+
157
+ user = task.async { DB[:users].where(id: id).first }
158
+ posts = task.async { DB[:posts].where(user_id: id).limit(10).all }
159
+ stats = task.async { DB[:stats].where(user_id: id).first }
160
+
161
+ [{
162
+ user: user.wait,
163
+ posts: posts.wait,
164
+ stats: stats.wait
165
+ }, 200]
166
+ end
167
+ ```
168
+
169
+ ## Migrations
170
+
171
+ FunApi doesn't include migrations. Use your database library's migration tool:
172
+
173
+ - Sequel: `sequel -m migrations/ postgres://...`
174
+ - ActiveRecord: `rake db:migrate`
175
+ - Raw SQL files with a migration runner