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,155 @@
1
+ ---
2
+ title: OpenAPI
3
+ ---
4
+
5
+ # OpenAPI
6
+
7
+ FunApi automatically generates OpenAPI 3.0 documentation from your routes and schemas.
8
+
9
+ ## Built-in Endpoints
10
+
11
+ Every FunApi application exposes:
12
+
13
+ | Endpoint | Description |
14
+ |----------|-------------|
15
+ | `/docs` | Interactive Swagger UI |
16
+ | `/openapi.json` | Raw OpenAPI specification |
17
+
18
+ ## Configuring Your API
19
+
20
+ Set API metadata when creating the app:
21
+
22
+ ```ruby
23
+ app = FunApi::App.new(
24
+ title: "User Management API",
25
+ version: "1.0.0",
26
+ description: "A comprehensive user management system"
27
+ ) do |api|
28
+ # routes...
29
+ end
30
+ ```
31
+
32
+ This appears in the OpenAPI spec and Swagger UI header.
33
+
34
+ ## What Gets Documented
35
+
36
+ ### Routes
37
+
38
+ All routes are automatically included:
39
+
40
+ ```ruby
41
+ api.get '/users' do |input, req, task|
42
+ # Documented as GET /users
43
+ end
44
+
45
+ api.post '/users' do |input, req, task|
46
+ # Documented as POST /users
47
+ end
48
+ ```
49
+
50
+ ### Path Parameters
51
+
52
+ Path parameters are extracted and documented:
53
+
54
+ ```ruby
55
+ api.get '/users/:id' do |input, req, task|
56
+ # Documented with {id} parameter
57
+ end
58
+ ```
59
+
60
+ ### Schemas
61
+
62
+ Schemas become OpenAPI components:
63
+
64
+ ```ruby
65
+ UserSchema = FunApi::Schema.define do
66
+ required(:name).filled(:string)
67
+ required(:email).filled(:string)
68
+ end
69
+
70
+ api.post '/users', body: UserSchema do |input, req, task|
71
+ # Request body documented with UserSchema
72
+ end
73
+ ```
74
+
75
+ ### Response Schemas
76
+
77
+ Response schemas document the output:
78
+
79
+ ```ruby
80
+ api.get '/users/:id', response_schema: UserOutputSchema do |input, req, task|
81
+ # Response documented with UserOutputSchema
82
+ end
83
+ ```
84
+
85
+ ## Swagger UI
86
+
87
+ The `/docs` endpoint serves an interactive Swagger UI where you can:
88
+
89
+ - Browse all endpoints
90
+ - See request/response schemas
91
+ - Try out API calls directly
92
+ - View example payloads
93
+
94
+ ## OpenAPI JSON
95
+
96
+ Access the raw spec at `/openapi.json`:
97
+
98
+ ```json
99
+ {
100
+ "openapi": "3.0.0",
101
+ "info": {
102
+ "title": "User Management API",
103
+ "version": "1.0.0",
104
+ "description": "A comprehensive user management system"
105
+ },
106
+ "paths": {
107
+ "/users": {
108
+ "get": { ... },
109
+ "post": { ... }
110
+ }
111
+ },
112
+ "components": {
113
+ "schemas": {
114
+ "UserSchema": { ... }
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ ## Schema Names
121
+
122
+ Schema names in OpenAPI come from your Ruby constant names:
123
+
124
+ ```ruby
125
+ UserCreateSchema = FunApi::Schema.define { ... }
126
+ # Becomes "UserCreateSchema" in OpenAPI
127
+
128
+ UserOutputSchema = FunApi::Schema.define { ... }
129
+ # Becomes "UserOutputSchema" in OpenAPI
130
+ ```
131
+
132
+ ## Use Cases
133
+
134
+ ### Client Generation
135
+
136
+ Use the OpenAPI spec to generate clients:
137
+
138
+ ```bash
139
+ # Generate TypeScript client
140
+ npx openapi-typescript http://localhost:3000/openapi.json -o api.ts
141
+
142
+ # Generate Python client
143
+ openapi-generator generate -i http://localhost:3000/openapi.json -g python
144
+ ```
145
+
146
+ ### API Testing
147
+
148
+ Import the spec into Postman, Insomnia, or other API tools.
149
+
150
+ ### Documentation Hosting
151
+
152
+ Export the spec and host on platforms like:
153
+ - SwaggerHub
154
+ - Redoc
155
+ - Stoplight
@@ -0,0 +1,123 @@
1
+ ---
2
+ title: Routing
3
+ ---
4
+
5
+ # Routing
6
+
7
+ Routes map HTTP requests to handler functions based on the path and method.
8
+
9
+ ## Defining Routes
10
+
11
+ FunApi provides methods for each HTTP verb:
12
+
13
+ ```ruby
14
+ api.get '/users' do |input, req, task|
15
+ [{ users: [] }, 200]
16
+ end
17
+
18
+ api.post '/users' do |input, req, task|
19
+ [{ created: input[:body] }, 201]
20
+ end
21
+
22
+ api.put '/users/:id' do |input, req, task|
23
+ [{ updated: true }, 200]
24
+ end
25
+
26
+ api.patch '/users/:id' do |input, req, task|
27
+ [{ patched: true }, 200]
28
+ end
29
+
30
+ api.delete '/users/:id' do |input, req, task|
31
+ [{}, 204]
32
+ end
33
+ ```
34
+
35
+ ## Path Parameters
36
+
37
+ Capture dynamic segments with `:param` syntax:
38
+
39
+ ```ruby
40
+ api.get '/users/:id' do |input, req, task|
41
+ user_id = input[:path]['id'] # Always a string
42
+ [{ id: user_id }, 200]
43
+ end
44
+
45
+ api.get '/posts/:post_id/comments/:comment_id' do |input, req, task|
46
+ post_id = input[:path]['post_id']
47
+ comment_id = input[:path]['comment_id']
48
+ [{ post_id: post_id, comment_id: comment_id }, 200]
49
+ end
50
+ ```
51
+
52
+ > **Note**: Path parameters are always strings. Convert them manually if needed:
53
+ > ```ruby
54
+ > id = input[:path]['id'].to_i
55
+ > ```
56
+
57
+ ## Query Parameters
58
+
59
+ Query parameters come from the URL query string:
60
+
61
+ ```ruby
62
+ # GET /search?q=ruby&limit=10
63
+ api.get '/search' do |input, req, task|
64
+ query = input[:query][:q]
65
+ limit = input[:query][:limit]&.to_i || 20
66
+ [{ query: query, limit: limit }, 200]
67
+ end
68
+ ```
69
+
70
+ With validation:
71
+
72
+ ```ruby
73
+ SearchSchema = FunApi::Schema.define do
74
+ required(:q).filled(:string)
75
+ optional(:limit).filled(:integer)
76
+ optional(:offset).filled(:integer)
77
+ end
78
+
79
+ api.get '/search', query: SearchSchema do |input, req, task|
80
+ # input[:query] is validated and coerced
81
+ [{ results: search(input[:query]) }, 200]
82
+ end
83
+ ```
84
+
85
+ ## Request Body
86
+
87
+ POST, PUT, and PATCH routes typically receive a JSON body:
88
+
89
+ ```ruby
90
+ UserSchema = FunApi::Schema.define do
91
+ required(:name).filled(:string)
92
+ required(:email).filled(:string)
93
+ end
94
+
95
+ api.post '/users', body: UserSchema do |input, req, task|
96
+ user = input[:body] # Validated hash
97
+ [{ created: user }, 201]
98
+ end
99
+ ```
100
+
101
+ ## Route Priority
102
+
103
+ Routes are matched in the order they're defined. More specific routes should come first:
104
+
105
+ ```ruby
106
+ api.get '/users/me' do |input, req, task|
107
+ # Matches /users/me
108
+ end
109
+
110
+ api.get '/users/:id' do |input, req, task|
111
+ # Matches /users/123, /users/anything
112
+ end
113
+ ```
114
+
115
+ ## The Root Route
116
+
117
+ The root path `/` works like any other route:
118
+
119
+ ```ruby
120
+ api.get '/' do |input, req, task|
121
+ [{ status: 'ok' }, 200]
122
+ end
123
+ ```
@@ -0,0 +1,166 @@
1
+ ---
2
+ title: Validation
3
+ ---
4
+
5
+ # Validation
6
+
7
+ FunApi uses [dry-schema](https://dry-rb.org/gems/dry-schema/) for request and response validation.
8
+
9
+ ## Defining Schemas
10
+
11
+ Create schemas with `FunApi::Schema.define`:
12
+
13
+ ```ruby
14
+ UserSchema = FunApi::Schema.define do
15
+ required(:name).filled(:string)
16
+ required(:email).filled(:string)
17
+ optional(:age).filled(:integer)
18
+ optional(:role).filled(:string)
19
+ end
20
+ ```
21
+
22
+ > **Technical Detail**: `FunApi::Schema.define` is a thin wrapper around `Dry::Schema.Params`. You have access to the full dry-schema DSL.
23
+
24
+ ## Applying Schemas
25
+
26
+ ### Body Validation
27
+
28
+ ```ruby
29
+ api.post '/users', body: UserSchema do |input, req, task|
30
+ user = input[:body] # Validated and coerced
31
+ [{ created: user }, 201]
32
+ end
33
+ ```
34
+
35
+ ### Query Validation
36
+
37
+ ```ruby
38
+ SearchSchema = FunApi::Schema.define do
39
+ required(:q).filled(:string)
40
+ optional(:limit).filled(:integer)
41
+ optional(:offset).filled(:integer)
42
+ end
43
+
44
+ api.get '/search', query: SearchSchema do |input, req, task|
45
+ [{ results: search(input[:query]) }, 200]
46
+ end
47
+ ```
48
+
49
+ ### Response Validation
50
+
51
+ Filter and validate response data:
52
+
53
+ ```ruby
54
+ UserOutputSchema = FunApi::Schema.define do
55
+ required(:id).filled(:integer)
56
+ required(:name).filled(:string)
57
+ required(:email).filled(:string)
58
+ end
59
+
60
+ api.get '/users/:id', response_schema: UserOutputSchema do |input, req, task|
61
+ user = find_user(input[:path]['id'])
62
+ # password and other fields are filtered out
63
+ [user, 200]
64
+ end
65
+ ```
66
+
67
+ ## Schema DSL
68
+
69
+ ### Required vs Optional
70
+
71
+ ```ruby
72
+ FunApi::Schema.define do
73
+ required(:name).filled(:string) # Must be present and non-empty
74
+ optional(:nickname).filled(:string) # Can be absent, but if present must be valid
75
+ end
76
+ ```
77
+
78
+ ### Types
79
+
80
+ ```ruby
81
+ FunApi::Schema.define do
82
+ required(:name).filled(:string)
83
+ required(:age).filled(:integer)
84
+ required(:price).filled(:float)
85
+ required(:active).filled(:bool)
86
+ required(:tags).filled(:array)
87
+ required(:metadata).filled(:hash)
88
+ end
89
+ ```
90
+
91
+ ### Nested Objects
92
+
93
+ ```ruby
94
+ AddressSchema = FunApi::Schema.define do
95
+ required(:street).filled(:string)
96
+ required(:city).filled(:string)
97
+ required(:zip).filled(:string)
98
+ end
99
+
100
+ UserSchema = FunApi::Schema.define do
101
+ required(:name).filled(:string)
102
+ required(:address).hash(AddressSchema)
103
+ end
104
+ ```
105
+
106
+ ### Arrays of Objects
107
+
108
+ ```ruby
109
+ ItemSchema = FunApi::Schema.define do
110
+ required(:name).filled(:string)
111
+ required(:quantity).filled(:integer)
112
+ end
113
+
114
+ OrderSchema = FunApi::Schema.define do
115
+ required(:items).array(:hash) do
116
+ required(:name).filled(:string)
117
+ required(:quantity).filled(:integer)
118
+ end
119
+ end
120
+ ```
121
+
122
+ Or validate an array of items:
123
+
124
+ ```ruby
125
+ api.post '/users/batch', body: [UserSchema] do |input, req, task|
126
+ users = input[:body] # Array of validated users
127
+ [{ created: users.length }, 201]
128
+ end
129
+ ```
130
+
131
+ ## Validation Errors
132
+
133
+ When validation fails, FunApi returns a FastAPI-style error response:
134
+
135
+ ```json
136
+ {
137
+ "detail": [
138
+ {
139
+ "loc": ["body", "email"],
140
+ "msg": "is missing",
141
+ "type": "value_error"
142
+ },
143
+ {
144
+ "loc": ["body", "age"],
145
+ "msg": "must be an integer",
146
+ "type": "value_error"
147
+ }
148
+ ]
149
+ }
150
+ ```
151
+
152
+ Status code: `422 Unprocessable Entity`
153
+
154
+ ## Custom Validation
155
+
156
+ For complex validation, use dry-schema's full DSL:
157
+
158
+ ```ruby
159
+ UserSchema = FunApi::Schema.define do
160
+ required(:email).filled(:string, format?: /@/)
161
+ required(:age).filled(:integer, gt?: 0, lt?: 150)
162
+ required(:password).filled(:string, min_size?: 8)
163
+ end
164
+ ```
165
+
166
+ See the [dry-schema documentation](https://dry-rb.org/gems/dry-schema/) for the complete DSL reference.
@@ -0,0 +1,82 @@
1
+ ---
2
+ title: At Glance
3
+ ---
4
+
5
+ # At Glance
6
+
7
+ ## What is FunApi?
8
+
9
+ FunApi is a minimal, async-first Ruby web framework for building APIs. It draws heavy inspiration from Python's FastAPI, bringing that same developer experience to Ruby.
10
+
11
+ ## Philosophy
12
+
13
+ <!--
14
+ TODO: Fill in your philosophy here. Some prompts:
15
+ - Why did you create this?
16
+ - What's wrong with existing Ruby frameworks for APIs?
17
+ - What makes async-first important?
18
+ - Who is this for?
19
+ -->
20
+
21
+ ### Async-first
22
+
23
+ FunApi is built from the ground up for async operations. Every route handler receives an `Async::Task` that enables true concurrent execution within your routes.
24
+
25
+ ### Minimal Magic
26
+
27
+ Unlike larger frameworks, FunApi keeps things explicit. No hidden callbacks, no implicit behavior. You see exactly what's happening.
28
+
29
+ ### Validation at the Edges
30
+
31
+ Request validation happens before your handler runs. Response filtering happens after. Your business logic stays clean.
32
+
33
+ ### Auto-Documentation
34
+
35
+ Your API documentation is generated from your code. Define a schema once, get validation AND documentation.
36
+
37
+ ## Where FunApi Fits
38
+
39
+ <!--
40
+ TODO: Fill in comparison with other Ruby frameworks:
41
+ - vs Rails API-only
42
+ - vs Sinatra
43
+ - vs Roda
44
+ - vs Grape
45
+ -->
46
+
47
+ | Framework | Use Case | FunApi Difference |
48
+ |-----------|----------|-------------------|
49
+ | Rails API | Full-featured API | FunApi is lighter, async-first |
50
+ | Sinatra | Simple APIs | FunApi adds validation, OpenAPI |
51
+ | Roda | Routing-focused | FunApi is async, has schemas |
52
+ | Grape | API-focused | FunApi is simpler, async |
53
+
54
+ ## Core Concepts Preview
55
+
56
+ ```ruby
57
+ app = FunApi::App.new do |api|
58
+ # Validation schemas
59
+ UserSchema = FunApi::Schema.define do
60
+ required(:name).filled(:string)
61
+ end
62
+
63
+ # Routes with validation
64
+ api.post '/users', body: UserSchema do |input, req, task|
65
+ # input[:body] is already validated
66
+ [{ user: input[:body] }, 201]
67
+ end
68
+
69
+ # Async operations
70
+ api.get '/dashboard/:id' do |input, req, task|
71
+ # Concurrent fetches
72
+ user = task.async { fetch_user(input[:path]['id']) }
73
+ posts = task.async { fetch_posts(input[:path]['id']) }
74
+
75
+ [{ user: user.wait, posts: posts.wait }, 200]
76
+ end
77
+
78
+ # Lifecycle hooks
79
+ api.on_startup { DB.connect }
80
+ api.on_shutdown { DB.disconnect }
81
+ end
82
+ ```
@@ -0,0 +1,150 @@
1
+ ---
2
+ title: Key Concepts
3
+ ---
4
+
5
+ # Key Concepts
6
+
7
+ Understanding these core concepts will help you work effectively with FunApi.
8
+
9
+ ## The App
10
+
11
+ Everything starts with `FunApi::App`. It's your application container that holds routes, middleware, and configuration.
12
+
13
+ ```ruby
14
+ app = FunApi::App.new(
15
+ title: "My API", # Shows in OpenAPI docs
16
+ version: "1.0.0", # API version
17
+ description: "..." # Optional description
18
+ ) do |api|
19
+ # Define routes here
20
+ end
21
+ ```
22
+
23
+ ## Route Handlers
24
+
25
+ Route handlers are blocks that receive three arguments:
26
+
27
+ ```ruby
28
+ api.get '/path' do |input, req, task|
29
+ # input - Hash with :path, :query, :body
30
+ # req - Rack::Request object
31
+ # task - Async::Task for concurrent operations
32
+
33
+ [response_data, status_code]
34
+ end
35
+ ```
36
+
37
+ ### The Input Hash
38
+
39
+ All request data is normalized into a single `input` hash:
40
+
41
+ ```ruby
42
+ api.post '/users/:id' do |input, req, task|
43
+ input[:path] # => { 'id' => '123' }
44
+ input[:query] # => { limit: 10, offset: 0 }
45
+ input[:body] # => { name: 'Alice', ... }
46
+ end
47
+ ```
48
+
49
+ ### Return Value
50
+
51
+ Handlers return a tuple of `[data, status_code]`:
52
+
53
+ ```ruby
54
+ [{ user: user }, 200] # Success
55
+ [{ error: 'Not found' }, 404] # Error
56
+ [created_user, 201] # Created
57
+ ```
58
+
59
+ ## Schemas
60
+
61
+ Schemas define the shape of request and response data using dry-schema:
62
+
63
+ ```ruby
64
+ UserSchema = FunApi::Schema.define do
65
+ required(:name).filled(:string)
66
+ required(:email).filled(:string)
67
+ optional(:age).filled(:integer)
68
+ end
69
+ ```
70
+
71
+ Apply schemas to routes:
72
+
73
+ ```ruby
74
+ api.post '/users', body: UserSchema do |input, req, task|
75
+ # input[:body] is validated and coerced
76
+ end
77
+
78
+ api.get '/users', query: QuerySchema do |input, req, task|
79
+ # input[:query] is validated
80
+ end
81
+ ```
82
+
83
+ ## Async Task
84
+
85
+ The `task` parameter is an `Async::Task` from Ruby's Async library. Use it for concurrent operations:
86
+
87
+ ```ruby
88
+ api.get '/dashboard' do |input, req, task|
89
+ # These run concurrently
90
+ user_task = task.async { fetch_user }
91
+ posts_task = task.async { fetch_posts }
92
+ stats_task = task.async { fetch_stats }
93
+
94
+ # Wait for all to complete
95
+ [{
96
+ user: user_task.wait,
97
+ posts: posts_task.wait,
98
+ stats: stats_task.wait
99
+ }, 200]
100
+ end
101
+ ```
102
+
103
+ ## Middleware Stack
104
+
105
+ Middleware wraps your application, processing requests before they reach handlers and responses after:
106
+
107
+ ```ruby
108
+ app = FunApi::App.new do |api|
109
+ # Built-in middleware
110
+ api.add_cors(allow_origins: ['*'])
111
+ api.add_request_logger
112
+
113
+ # Standard Rack middleware
114
+ api.use Rack::Session::Cookie, secret: 'key'
115
+
116
+ # Routes...
117
+ end
118
+ ```
119
+
120
+ Middleware runs in order: first added runs first.
121
+
122
+ ## Lifecycle Hooks
123
+
124
+ Run code when the application starts or stops:
125
+
126
+ ```ruby
127
+ app = FunApi::App.new do |api|
128
+ api.on_startup do
129
+ DB.connect
130
+ Cache.warm
131
+ end
132
+
133
+ api.on_shutdown do
134
+ DB.disconnect
135
+ end
136
+ end
137
+ ```
138
+
139
+ ## OpenAPI/Swagger
140
+
141
+ FunApi automatically generates OpenAPI documentation from your routes and schemas:
142
+
143
+ - `/docs` - Interactive Swagger UI
144
+ - `/openapi.json` - Raw OpenAPI specification
145
+
146
+ The docs are generated from:
147
+ - Route paths and methods
148
+ - Path parameters
149
+ - Query and body schemas
150
+ - Response schemas