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,141 @@
1
+ ---
2
+ title: Dependencies
3
+ ---
4
+
5
+ # Dependencies
6
+
7
+ Dependency injection lets you provide services to your handlers without global state.
8
+
9
+ ## Registering Dependencies
10
+
11
+ Register dependencies in the app container:
12
+
13
+ ```ruby
14
+ app = FunApi::App.new do |api|
15
+ api.register(:db) { Database.connect }
16
+ api.register(:logger) { Logger.new(STDOUT) }
17
+ api.register(:mailer) { Mailer.new }
18
+ end
19
+ ```
20
+
21
+ ## Using Dependencies
22
+
23
+ Request dependencies with the `depends:` parameter:
24
+
25
+ ```ruby
26
+ api.get '/users', depends: [:db] do |input, req, task, db:|
27
+ users = db.query("SELECT * FROM users")
28
+ [{ users: users }, 200]
29
+ end
30
+
31
+ api.post '/contact', depends: [:mailer, :logger] do |input, req, task, mailer:, logger:|
32
+ logger.info("Sending contact email")
33
+ mailer.send(input[:body])
34
+ [{ sent: true }, 200]
35
+ end
36
+ ```
37
+
38
+ ## Dependency Cleanup
39
+
40
+ For resources that need cleanup (database connections, file handles), return a tuple:
41
+
42
+ ```ruby
43
+ api.register(:db) do
44
+ conn = Database.connect
45
+ cleanup = -> { conn.close }
46
+ [conn, cleanup]
47
+ end
48
+ ```
49
+
50
+ The cleanup proc runs after the request completes.
51
+
52
+ ## Block-Style Dependencies
53
+
54
+ For context-manager style cleanup (like Python's `with`):
55
+
56
+ ```ruby
57
+ api.register(:transaction) do |yielder|
58
+ db = Database.connect
59
+ db.begin_transaction
60
+
61
+ yielder.call(db) # Yield the resource
62
+
63
+ db.commit
64
+ rescue
65
+ db.rollback
66
+ raise
67
+ ensure
68
+ db.close
69
+ end
70
+ ```
71
+
72
+ ## Per-Request Dependencies
73
+
74
+ Dependencies can access request context:
75
+
76
+ ```ruby
77
+ api.register(:current_user) do
78
+ # This runs fresh for each request
79
+ User.find_by_token(request.headers['Authorization'])
80
+ end
81
+ ```
82
+
83
+ ## Depends Class
84
+
85
+ For complex dependency graphs, use `FunApi::Depends`:
86
+
87
+ ```ruby
88
+ get_db = -> { Database.connect }
89
+ get_user = ->(db:) { db.find_user(current_token) }
90
+
91
+ api.get '/profile', depends: {
92
+ db: get_db,
93
+ user: FunApi.Depends(get_user, db: :db)
94
+ } do |input, req, task, db:, user:|
95
+ [{ user: user }, 200]
96
+ end
97
+ ```
98
+
99
+ ## Complete Example
100
+
101
+ ```ruby
102
+ require 'funapi'
103
+ require 'funapi/server/falcon'
104
+
105
+ app = FunApi::App.new(title: "My API") do |api|
106
+ # Simple dependency
107
+ api.register(:logger) { Logger.new(STDOUT) }
108
+
109
+ # Dependency with cleanup
110
+ api.register(:db) do
111
+ conn = PG.connect(ENV['DATABASE_URL'])
112
+ [conn, -> { conn.close }]
113
+ end
114
+
115
+ # Block-style dependency
116
+ api.register(:transaction) do |yielder|
117
+ conn = PG.connect(ENV['DATABASE_URL'])
118
+ conn.exec("BEGIN")
119
+ yielder.call(conn)
120
+ conn.exec("COMMIT")
121
+ rescue
122
+ conn.exec("ROLLBACK")
123
+ raise
124
+ ensure
125
+ conn.close
126
+ end
127
+
128
+ api.get '/users', depends: [:db, :logger] do |input, req, task, db:, logger:|
129
+ logger.info("Fetching users")
130
+ result = db.exec("SELECT * FROM users")
131
+ [{ users: result.to_a }, 200]
132
+ end
133
+
134
+ api.post '/users', depends: [:transaction] do |input, req, task, transaction:|
135
+ transaction.exec("INSERT INTO users (name) VALUES ($1)", [input[:body][:name]])
136
+ [{ created: true }, 201]
137
+ end
138
+ end
139
+
140
+ FunApi::Server::Falcon.start(app, port: 3000)
141
+ ```
@@ -0,0 +1,212 @@
1
+ ---
2
+ title: Deployment
3
+ ---
4
+
5
+ # Deployment
6
+
7
+ Deploy FunApi applications to production.
8
+
9
+ ## Running with Falcon
10
+
11
+ FunApi uses Falcon as its server:
12
+
13
+ ```ruby
14
+ # app.rb
15
+ require 'funapi'
16
+ require 'funapi/server/falcon'
17
+
18
+ app = FunApi::App.new do |api|
19
+ # routes...
20
+ end
21
+
22
+ FunApi::Server::Falcon.start(app,
23
+ host: '0.0.0.0',
24
+ port: ENV.fetch('PORT', 3000).to_i
25
+ )
26
+ ```
27
+
28
+ ```bash
29
+ ruby app.rb
30
+ ```
31
+
32
+ ## Environment Variables
33
+
34
+ Configure your app with environment variables:
35
+
36
+ ```ruby
37
+ app = FunApi::App.new do |api|
38
+ api.on_startup do
39
+ DB.connect(ENV.fetch('DATABASE_URL'))
40
+ end
41
+
42
+ if ENV['RACK_ENV'] == 'production'
43
+ api.add_trusted_host(allowed_hosts: [ENV['HOST']])
44
+ end
45
+ end
46
+ ```
47
+
48
+ ## Docker
49
+
50
+ ### Dockerfile
51
+
52
+ ```dockerfile
53
+ FROM ruby:3.2-alpine
54
+
55
+ RUN apk add --no-cache build-base
56
+
57
+ WORKDIR /app
58
+
59
+ COPY Gemfile Gemfile.lock ./
60
+ RUN bundle install --without development test
61
+
62
+ COPY . .
63
+
64
+ EXPOSE 3000
65
+
66
+ CMD ["ruby", "app.rb"]
67
+ ```
68
+
69
+ ### docker-compose.yml
70
+
71
+ ```yaml
72
+ version: '3.8'
73
+
74
+ services:
75
+ web:
76
+ build: .
77
+ ports:
78
+ - "3000:3000"
79
+ environment:
80
+ - DATABASE_URL=postgres://postgres:password@db/myapp
81
+ - RACK_ENV=production
82
+ depends_on:
83
+ - db
84
+
85
+ db:
86
+ image: postgres:15
87
+ environment:
88
+ - POSTGRES_PASSWORD=password
89
+ - POSTGRES_DB=myapp
90
+ volumes:
91
+ - postgres_data:/var/lib/postgresql/data
92
+
93
+ volumes:
94
+ postgres_data:
95
+ ```
96
+
97
+ ## Fly.io
98
+
99
+ ### fly.toml
100
+
101
+ ```toml
102
+ app = "my-funapi-app"
103
+ primary_region = "iad"
104
+
105
+ [build]
106
+ builder = "heroku/buildpacks:22"
107
+
108
+ [env]
109
+ RACK_ENV = "production"
110
+
111
+ [http_service]
112
+ internal_port = 3000
113
+ force_https = true
114
+ auto_stop_machines = true
115
+ auto_start_machines = true
116
+ ```
117
+
118
+ Deploy:
119
+
120
+ ```bash
121
+ fly launch
122
+ fly deploy
123
+ ```
124
+
125
+ ## Render
126
+
127
+ Create a `render.yaml`:
128
+
129
+ ```yaml
130
+ services:
131
+ - type: web
132
+ name: my-funapi-app
133
+ env: ruby
134
+ buildCommand: bundle install
135
+ startCommand: ruby app.rb
136
+ envVars:
137
+ - key: RACK_ENV
138
+ value: production
139
+ - key: DATABASE_URL
140
+ fromDatabase:
141
+ name: mydb
142
+ property: connectionString
143
+ ```
144
+
145
+ ## Production Checklist
146
+
147
+ ### Security
148
+
149
+ ```ruby
150
+ app = FunApi::App.new do |api|
151
+ # Validate host header
152
+ api.add_trusted_host(allowed_hosts: ['myapp.com', 'api.myapp.com'])
153
+
154
+ # CORS with specific origins
155
+ api.add_cors(allow_origins: ['https://myapp.com'])
156
+ end
157
+ ```
158
+
159
+ ### Logging
160
+
161
+ ```ruby
162
+ api.add_request_logger(
163
+ logger: Logger.new(STDOUT),
164
+ level: :info
165
+ )
166
+ ```
167
+
168
+ ### Error Handling
169
+
170
+ Don't expose internal errors in production:
171
+
172
+ ```ruby
173
+ class ProductionErrorHandler
174
+ def initialize(app)
175
+ @app = app
176
+ end
177
+
178
+ def call(env)
179
+ @app.call(env)
180
+ rescue => e
181
+ # Log the real error
182
+ Logger.new(STDOUT).error("#{e.class}: #{e.message}")
183
+
184
+ # Return generic message
185
+ [500, {'content-type' => 'application/json'},
186
+ ['{"detail":"Internal server error"}']]
187
+ end
188
+ end
189
+
190
+ app = FunApi::App.new do |api|
191
+ api.use ProductionErrorHandler if ENV['RACK_ENV'] == 'production'
192
+ end
193
+ ```
194
+
195
+ ### Health Check
196
+
197
+ ```ruby
198
+ api.get '/health' do |input, req, task|
199
+ [{ status: 'ok', time: Time.now.iso8601 }, 200]
200
+ end
201
+ ```
202
+
203
+ ### Graceful Shutdown
204
+
205
+ FunApi handles SIGINT and SIGTERM automatically, running shutdown hooks before exiting.
206
+
207
+ ```ruby
208
+ api.on_shutdown do
209
+ puts "Draining connections..."
210
+ ConnectionPool.shutdown
211
+ end
212
+ ```
@@ -0,0 +1,184 @@
1
+ ---
2
+ title: Error Handling
3
+ ---
4
+
5
+ # Error Handling
6
+
7
+ FunApi provides FastAPI-style error responses.
8
+
9
+ ## HTTPException
10
+
11
+ Raise `HTTPException` to return an error response:
12
+
13
+ ```ruby
14
+ api.get '/users/:id' do |input, req, task|
15
+ user = find_user(input[:path]['id'])
16
+
17
+ unless user
18
+ raise FunApi::HTTPException.new(
19
+ status_code: 404,
20
+ detail: "User not found"
21
+ )
22
+ end
23
+
24
+ [{ user: user }, 200]
25
+ end
26
+ ```
27
+
28
+ Response:
29
+
30
+ ```json
31
+ {
32
+ "detail": "User not found"
33
+ }
34
+ ```
35
+
36
+ ## Common Status Codes
37
+
38
+ ```ruby
39
+ # 400 Bad Request
40
+ raise FunApi::HTTPException.new(status_code: 400, detail: "Invalid input")
41
+
42
+ # 401 Unauthorized
43
+ raise FunApi::HTTPException.new(status_code: 401, detail: "Authentication required")
44
+
45
+ # 403 Forbidden
46
+ raise FunApi::HTTPException.new(status_code: 403, detail: "Permission denied")
47
+
48
+ # 404 Not Found
49
+ raise FunApi::HTTPException.new(status_code: 404, detail: "Resource not found")
50
+
51
+ # 409 Conflict
52
+ raise FunApi::HTTPException.new(status_code: 409, detail: "Already exists")
53
+
54
+ # 422 Unprocessable Entity
55
+ raise FunApi::HTTPException.new(status_code: 422, detail: "Validation failed")
56
+
57
+ # 500 Internal Server Error
58
+ raise FunApi::HTTPException.new(status_code: 500, detail: "Something went wrong")
59
+ ```
60
+
61
+ ## Custom Headers
62
+
63
+ Add headers to error responses:
64
+
65
+ ```ruby
66
+ raise FunApi::HTTPException.new(
67
+ status_code: 401,
68
+ detail: "Token expired",
69
+ headers: { 'WWW-Authenticate' => 'Bearer' }
70
+ )
71
+ ```
72
+
73
+ ## ValidationError
74
+
75
+ `ValidationError` is raised automatically by schema validation, but you can raise it manually:
76
+
77
+ ```ruby
78
+ raise FunApi::ValidationError.new(
79
+ errors: [
80
+ { path: [:email], text: "is invalid" }
81
+ ]
82
+ )
83
+ ```
84
+
85
+ Response (422):
86
+
87
+ ```json
88
+ {
89
+ "detail": [
90
+ {
91
+ "loc": ["email"],
92
+ "msg": "is invalid",
93
+ "type": "value_error"
94
+ }
95
+ ]
96
+ }
97
+ ```
98
+
99
+ ## Handling Exceptions in Handlers
100
+
101
+ Use standard Ruby exception handling:
102
+
103
+ ```ruby
104
+ api.get '/external' do |input, req, task|
105
+ begin
106
+ data = ExternalAPI.fetch
107
+ [{ data: data }, 200]
108
+ rescue ExternalAPI::Timeout
109
+ raise FunApi::HTTPException.new(
110
+ status_code: 504,
111
+ detail: "External service timeout"
112
+ )
113
+ rescue ExternalAPI::Error => e
114
+ raise FunApi::HTTPException.new(
115
+ status_code: 502,
116
+ detail: "External service error: #{e.message}"
117
+ )
118
+ end
119
+ end
120
+ ```
121
+
122
+ ## Custom Error Classes
123
+
124
+ Create domain-specific errors:
125
+
126
+ ```ruby
127
+ class NotFoundError < FunApi::HTTPException
128
+ def initialize(resource, id)
129
+ super(
130
+ status_code: 404,
131
+ detail: "#{resource} with id #{id} not found"
132
+ )
133
+ end
134
+ end
135
+
136
+ class UnauthorizedError < FunApi::HTTPException
137
+ def initialize(message = "Authentication required")
138
+ super(
139
+ status_code: 401,
140
+ detail: message,
141
+ headers: { 'WWW-Authenticate' => 'Bearer' }
142
+ )
143
+ end
144
+ end
145
+
146
+ # Usage
147
+ api.get '/users/:id' do |input, req, task|
148
+ user = find_user(input[:path]['id'])
149
+ raise NotFoundError.new('User', input[:path]['id']) unless user
150
+ [{ user: user }, 200]
151
+ end
152
+ ```
153
+
154
+ ## Error Middleware
155
+
156
+ Handle errors globally with middleware:
157
+
158
+ ```ruby
159
+ class ErrorHandlerMiddleware
160
+ def initialize(app)
161
+ @app = app
162
+ end
163
+
164
+ def call(env)
165
+ @app.call(env)
166
+ rescue StandardError => e
167
+ # Log the error
168
+ puts "Error: #{e.message}"
169
+ puts e.backtrace.first(5).join("\n")
170
+
171
+ # Return generic error in production
172
+ [
173
+ 500,
174
+ { 'content-type' => 'application/json' },
175
+ [JSON.dump(detail: 'Internal server error')]
176
+ ]
177
+ end
178
+ end
179
+
180
+ app = FunApi::App.new do |api|
181
+ api.use ErrorHandlerMiddleware
182
+ # routes...
183
+ end
184
+ ```
@@ -0,0 +1,159 @@
1
+ ---
2
+ title: Response Schema
3
+ ---
4
+
5
+ # Response Schema
6
+
7
+ Filter and validate response data before sending to clients.
8
+
9
+ ## Why Response Schemas?
10
+
11
+ Response schemas help you:
12
+
13
+ 1. **Filter sensitive data** - Remove passwords, tokens, internal IDs
14
+ 2. **Validate output** - Catch bugs before they reach clients
15
+ 3. **Document responses** - Auto-generate OpenAPI response schemas
16
+
17
+ ## Basic Usage
18
+
19
+ Define a schema for the response:
20
+
21
+ ```ruby
22
+ UserOutputSchema = FunApi::Schema.define do
23
+ required(:id).filled(:integer)
24
+ required(:name).filled(:string)
25
+ required(:email).filled(:string)
26
+ end
27
+
28
+ api.get '/users/:id', response_schema: UserOutputSchema do |input, req, task|
29
+ user = find_user(input[:path]['id'])
30
+ # Even if user has :password, :api_key, etc., they're filtered out
31
+ [user, 200]
32
+ end
33
+ ```
34
+
35
+ ## Filtering Sensitive Data
36
+
37
+ ```ruby
38
+ # Internal user record
39
+ user = {
40
+ id: 1,
41
+ name: "Alice",
42
+ email: "alice@example.com",
43
+ password_hash: "abc123...",
44
+ api_key: "secret...",
45
+ internal_notes: "VIP customer"
46
+ }
47
+
48
+ # With response_schema: UserOutputSchema
49
+ # Client receives only:
50
+ {
51
+ "id": 1,
52
+ "name": "Alice",
53
+ "email": "alice@example.com"
54
+ }
55
+ ```
56
+
57
+ ## Array Responses
58
+
59
+ For array responses, wrap the schema in brackets:
60
+
61
+ ```ruby
62
+ api.get '/users', response_schema: [UserOutputSchema] do |input, req, task|
63
+ users = fetch_all_users
64
+ [users, 200]
65
+ end
66
+ ```
67
+
68
+ ## Different Input/Output Schemas
69
+
70
+ Common pattern: accept more fields than you return.
71
+
72
+ ```ruby
73
+ UserCreateSchema = FunApi::Schema.define do
74
+ required(:name).filled(:string)
75
+ required(:email).filled(:string)
76
+ required(:password).filled(:string)
77
+ end
78
+
79
+ UserOutputSchema = FunApi::Schema.define do
80
+ required(:id).filled(:integer)
81
+ required(:name).filled(:string)
82
+ required(:email).filled(:string)
83
+ required(:created_at).filled(:string)
84
+ end
85
+
86
+ api.post '/users',
87
+ body: UserCreateSchema,
88
+ response_schema: UserOutputSchema do |input, req, task|
89
+
90
+ user = create_user(input[:body])
91
+ # password is filtered out of response
92
+ [user, 201]
93
+ end
94
+ ```
95
+
96
+ ## Nested Objects
97
+
98
+ ```ruby
99
+ AddressSchema = FunApi::Schema.define do
100
+ required(:city).filled(:string)
101
+ required(:country).filled(:string)
102
+ end
103
+
104
+ UserWithAddressSchema = FunApi::Schema.define do
105
+ required(:id).filled(:integer)
106
+ required(:name).filled(:string)
107
+ required(:address).hash do
108
+ required(:city).filled(:string)
109
+ required(:country).filled(:string)
110
+ end
111
+ end
112
+
113
+ api.get '/users/:id', response_schema: UserWithAddressSchema do |input, req, task|
114
+ user = find_user_with_address(input[:path]['id'])
115
+ [user, 200]
116
+ end
117
+ ```
118
+
119
+ ## Validation Errors
120
+
121
+ If your response doesn't match the schema, FunApi returns a 500 error:
122
+
123
+ ```ruby
124
+ api.get '/broken', response_schema: UserOutputSchema do |input, req, task|
125
+ # Missing required :email field
126
+ [{ id: 1, name: "Alice" }, 200]
127
+ end
128
+
129
+ # Response: 500
130
+ # {"detail":"Response validation failed: {:email=>[\"is missing\"]}"}
131
+ ```
132
+
133
+ This helps catch bugs in development before they reach production.
134
+
135
+ ## OpenAPI Integration
136
+
137
+ Response schemas appear in your OpenAPI documentation:
138
+
139
+ ```json
140
+ {
141
+ "paths": {
142
+ "/users/{id}": {
143
+ "get": {
144
+ "responses": {
145
+ "200": {
146
+ "content": {
147
+ "application/json": {
148
+ "schema": {
149
+ "$ref": "#/components/schemas/UserOutputSchema"
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+ }
158
+ }
159
+ ```