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
data/docs-site/app.rb ADDED
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "funapi"
4
+ require "funapi/templates"
5
+ require "funapi/server/falcon"
6
+ require "kramdown"
7
+ require "kramdown-parser-gfm"
8
+ require "rouge"
9
+
10
+ class DocsRenderer
11
+ def initialize(content_dir:)
12
+ @content_dir = Pathname.new(content_dir)
13
+ @cache = {}
14
+ end
15
+
16
+ def render(path)
17
+ file_path = @content_dir.join("#{path}.md")
18
+ raise FunApi::HTTPException.new(status_code: 404, detail: "Page not found") unless file_path.exist?
19
+
20
+ @cache[path] ||= begin
21
+ content = file_path.read
22
+ frontmatter, body = parse_frontmatter(content)
23
+ html = Kramdown::Document.new(
24
+ body,
25
+ input: "GFM",
26
+ syntax_highlighter: "rouge",
27
+ syntax_highlighter_opts: {default_lang: "ruby"}
28
+ ).to_html
29
+
30
+ {frontmatter: frontmatter, html: html}
31
+ end
32
+ end
33
+
34
+ def navigation
35
+ @navigation ||= build_navigation
36
+ end
37
+
38
+ private
39
+
40
+ def parse_frontmatter(content)
41
+ if content.start_with?("---")
42
+ parts = content.split("---", 3)
43
+ frontmatter = parse_yaml(parts[1])
44
+ body = parts[2]
45
+ [frontmatter, body]
46
+ else
47
+ [{}, content]
48
+ end
49
+ end
50
+
51
+ def parse_yaml(yaml_str)
52
+ result = {}
53
+ yaml_str.each_line do |line|
54
+ result[::Regexp.last_match(1).to_sym] = ::Regexp.last_match(2).strip if line =~ /^(\w+):\s*(.+)$/
55
+ end
56
+ result
57
+ end
58
+
59
+ def build_navigation
60
+ [
61
+ {
62
+ title: "Getting Started",
63
+ items: [
64
+ {path: "getting-started/at-glance", title: "At Glance"},
65
+ {path: "getting-started/quick-start", title: "Quick Start"},
66
+ {path: "getting-started/key-concepts", title: "Key Concepts"}
67
+ ]
68
+ },
69
+ {
70
+ title: "Essential",
71
+ items: [
72
+ {path: "essential/routing", title: "Routing"},
73
+ {path: "essential/handler", title: "Handler"},
74
+ {path: "essential/validation", title: "Validation"},
75
+ {path: "essential/openapi", title: "OpenAPI"},
76
+ {path: "essential/lifecycle", title: "Lifecycle"},
77
+ {path: "essential/middleware", title: "Middleware"}
78
+ ]
79
+ },
80
+ {
81
+ title: "Patterns",
82
+ items: [
83
+ {path: "patterns/async-operations", title: "Async Operations"},
84
+ {path: "patterns/dependencies", title: "Dependencies"},
85
+ {path: "patterns/background-tasks", title: "Background Tasks"},
86
+ {path: "patterns/templates", title: "Templates"},
87
+ {path: "patterns/error-handling", title: "Error Handling"},
88
+ {path: "patterns/response-schema", title: "Response Schema"},
89
+ {path: "patterns/database", title: "Database"},
90
+ {path: "patterns/testing", title: "Testing"},
91
+ {path: "patterns/deployment", title: "Deployment"}
92
+ ]
93
+ }
94
+ ]
95
+ end
96
+ end
97
+
98
+ # Get the docs-site directory path relative to this script
99
+ docs_site_dir = __dir__
100
+ docs = DocsRenderer.new(content_dir: File.join(docs_site_dir, "content"))
101
+ templates = FunApi::Templates.new(
102
+ directory: File.join(docs_site_dir, "templates"),
103
+ layout: "layouts/docs.html.erb"
104
+ )
105
+
106
+ app = FunApi::App.new(
107
+ title: "FunApi Documentation",
108
+ version: "0.1.0",
109
+ description: "Documentation for the FunApi framework"
110
+ ) do |api|
111
+ api.use Rack::Static, urls: ["/css"], root: File.join(docs_site_dir, "public")
112
+ api.add_request_logger
113
+
114
+ api.get "/" do |_input, _req, _task|
115
+ page = docs.render("index")
116
+ templates.response(
117
+ "page.html.erb",
118
+ title: page[:frontmatter][:title] || "FunApi",
119
+ content: page[:html],
120
+ nav: docs.navigation,
121
+ current_path: "index"
122
+ )
123
+ end
124
+
125
+ api.get "/docs/:section/:page" do |input, _req, _task|
126
+ path = "#{input[:path]["section"]}/#{input[:path]["page"]}"
127
+ page = docs.render(path)
128
+ templates.response(
129
+ "page.html.erb",
130
+ title: page[:frontmatter][:title] || "FunApi",
131
+ content: page[:html],
132
+ nav: docs.navigation,
133
+ current_path: path
134
+ )
135
+ end
136
+ end
137
+
138
+ FunApi::Server::Falcon.start(app, port: 3000) if __FILE__ == $0
@@ -0,0 +1,156 @@
1
+ ---
2
+ title: Handler
3
+ ---
4
+
5
+ # Handler
6
+
7
+ The handler is the function that processes a request and returns a response.
8
+
9
+ ## Handler Signature
10
+
11
+ Every handler receives three positional arguments:
12
+
13
+ ```ruby
14
+ api.get '/path' do |input, req, task|
15
+ [response_data, status_code]
16
+ end
17
+ ```
18
+
19
+ | Argument | Type | Description |
20
+ |----------|------|-------------|
21
+ | `input` | Hash | Request data (path, query, body) |
22
+ | `req` | Rack::Request | Full Rack request object |
23
+ | `task` | Async::Task | Current async task for concurrency |
24
+
25
+ ## The Input Hash
26
+
27
+ The `input` hash normalizes all request data:
28
+
29
+ ```ruby
30
+ api.post '/users/:id' do |input, req, task|
31
+ input[:path] # Path parameters: { 'id' => '123' }
32
+ input[:query] # Query params: { search: 'ruby' }
33
+ input[:body] # Parsed JSON body: { name: 'Alice' }
34
+ end
35
+ ```
36
+
37
+ ### Accessing Path Parameters
38
+
39
+ ```ruby
40
+ api.get '/posts/:post_id/comments/:id' do |input, req, task|
41
+ post_id = input[:path]['post_id']
42
+ comment_id = input[:path]['id']
43
+ # ...
44
+ end
45
+ ```
46
+
47
+ ### Accessing Query Parameters
48
+
49
+ ```ruby
50
+ # GET /search?q=ruby&page=2
51
+ api.get '/search' do |input, req, task|
52
+ query = input[:query][:q]
53
+ page = input[:query][:page]
54
+ # ...
55
+ end
56
+ ```
57
+
58
+ ### Accessing Request Body
59
+
60
+ ```ruby
61
+ api.post '/users' do |input, req, task|
62
+ name = input[:body][:name]
63
+ email = input[:body][:email]
64
+ # ...
65
+ end
66
+ ```
67
+
68
+ ## The Rack Request
69
+
70
+ The `req` object is a standard `Rack::Request`:
71
+
72
+ ```ruby
73
+ api.get '/info' do |input, req, task|
74
+ {
75
+ method: req.request_method,
76
+ path: req.path_info,
77
+ host: req.host,
78
+ content_type: req.content_type,
79
+ user_agent: req.user_agent,
80
+ ip: req.ip
81
+ }
82
+ end
83
+ ```
84
+
85
+ ### Accessing Headers
86
+
87
+ ```ruby
88
+ api.get '/auth' do |input, req, task|
89
+ auth_header = req.get_header('HTTP_AUTHORIZATION')
90
+ # or
91
+ auth_header = req.env['HTTP_AUTHORIZATION']
92
+ end
93
+ ```
94
+
95
+ ## The Async Task
96
+
97
+ The `task` parameter enables concurrent operations:
98
+
99
+ ```ruby
100
+ api.get '/dashboard' do |input, req, task|
101
+ # Run operations concurrently
102
+ user = task.async { UserService.find(id) }
103
+ posts = task.async { PostService.recent }
104
+
105
+ [{
106
+ user: user.wait,
107
+ posts: posts.wait
108
+ }, 200]
109
+ end
110
+ ```
111
+
112
+ See [Async Operations](/docs/patterns/async-operations) for more.
113
+
114
+ ## Return Value
115
+
116
+ Handlers must return `[data, status_code]`:
117
+
118
+ ```ruby
119
+ # JSON response
120
+ [{ message: 'Hello' }, 200]
121
+
122
+ # Array response
123
+ [users, 200]
124
+
125
+ # Empty response
126
+ [{}, 204]
127
+
128
+ # Error response
129
+ [{ error: 'Not found' }, 404]
130
+ ```
131
+
132
+ ### Returning HTML
133
+
134
+ Return a `TemplateResponse` for HTML:
135
+
136
+ ```ruby
137
+ api.get '/' do |input, req, task|
138
+ templates.response('home.html.erb', title: 'Home')
139
+ end
140
+ ```
141
+
142
+ See [Templates](/docs/patterns/templates) for more.
143
+
144
+ ## Keyword Arguments (Dependencies)
145
+
146
+ When using dependency injection, dependencies come as keyword arguments:
147
+
148
+ ```ruby
149
+ api.get '/users', depends: [:db, :logger] do |input, req, task, db:, logger:|
150
+ logger.info("Fetching users")
151
+ users = db.query("SELECT * FROM users")
152
+ [{ users: users }, 200]
153
+ end
154
+ ```
155
+
156
+ See [Dependencies](/docs/patterns/dependencies) for more.
@@ -0,0 +1,161 @@
1
+ ---
2
+ title: Lifecycle
3
+ ---
4
+
5
+ # Lifecycle
6
+
7
+ Lifecycle hooks let you run code when your application starts up or shuts down.
8
+
9
+ ## Startup Hooks
10
+
11
+ Run code before the server accepts requests:
12
+
13
+ ```ruby
14
+ app = FunApi::App.new do |api|
15
+ api.on_startup do
16
+ puts "Connecting to database..."
17
+ DB.connect
18
+ end
19
+
20
+ api.on_startup do
21
+ puts "Warming cache..."
22
+ Cache.warm
23
+ end
24
+ end
25
+ ```
26
+
27
+ ### Use Cases
28
+
29
+ - Database connection pool initialization
30
+ - Cache warming
31
+ - Loading configuration
32
+ - Starting background workers
33
+ - Metrics/logging initialization
34
+
35
+ ## Shutdown Hooks
36
+
37
+ Run code when the server stops:
38
+
39
+ ```ruby
40
+ app = FunApi::App.new do |api|
41
+ api.on_shutdown do
42
+ puts "Closing database connections..."
43
+ DB.disconnect
44
+ end
45
+
46
+ api.on_shutdown do
47
+ puts "Flushing metrics..."
48
+ Metrics.flush
49
+ end
50
+ end
51
+ ```
52
+
53
+ ### Use Cases
54
+
55
+ - Graceful database disconnection
56
+ - Flushing buffers/queues
57
+ - Stopping background workers
58
+ - Cleanup of temporary files
59
+
60
+ ## Multiple Hooks
61
+
62
+ You can register multiple hooks of each type. They run in registration order:
63
+
64
+ ```ruby
65
+ app = FunApi::App.new do |api|
66
+ api.on_startup do
67
+ puts "1. First startup hook"
68
+ end
69
+
70
+ api.on_startup do
71
+ puts "2. Second startup hook"
72
+ end
73
+
74
+ api.on_shutdown do
75
+ puts "1. First shutdown hook"
76
+ end
77
+
78
+ api.on_shutdown do
79
+ puts "2. Second shutdown hook"
80
+ end
81
+ end
82
+ ```
83
+
84
+ ## Error Handling
85
+
86
+ ### Startup Errors
87
+
88
+ If a startup hook raises an error, the server won't start:
89
+
90
+ ```ruby
91
+ api.on_startup do
92
+ raise "Database unavailable"
93
+ # Server fails to start
94
+ end
95
+ ```
96
+
97
+ ### Shutdown Errors
98
+
99
+ Shutdown hook errors are logged but don't stop other hooks:
100
+
101
+ ```ruby
102
+ api.on_shutdown do
103
+ raise "Cleanup failed"
104
+ # Error logged, but next hook still runs
105
+ end
106
+
107
+ api.on_shutdown do
108
+ puts "This still runs"
109
+ end
110
+ ```
111
+
112
+ ## Complete Example
113
+
114
+ ```ruby
115
+ require 'funapi'
116
+ require 'funapi/server/falcon'
117
+
118
+ app = FunApi::App.new(title: "My API") do |api|
119
+ api.on_startup do
120
+ puts "Starting up..."
121
+ $db = Database.connect(ENV['DATABASE_URL'])
122
+ $cache = Cache.new
123
+ $cache.warm
124
+ puts "Ready!"
125
+ end
126
+
127
+ api.on_shutdown do
128
+ puts "Shutting down..."
129
+ $cache.flush
130
+ $db.disconnect
131
+ puts "Goodbye!"
132
+ end
133
+
134
+ api.get '/health' do |input, req, task|
135
+ [{ status: 'ok', db: $db.connected? }, 200]
136
+ end
137
+ end
138
+
139
+ FunApi::Server::Falcon.start(app, port: 3000)
140
+ ```
141
+
142
+ ## With Dependencies
143
+
144
+ Combine lifecycle hooks with dependency injection:
145
+
146
+ ```ruby
147
+ app = FunApi::App.new do |api|
148
+ api.on_startup do
149
+ db_pool = ConnectionPool.new(size: 10) { Database.connect }
150
+ api.register(:db) { db_pool.checkout }
151
+ end
152
+
153
+ api.on_shutdown do
154
+ api.resolve(:db).close_all
155
+ end
156
+
157
+ api.get '/users', depends: [:db] do |input, req, task, db:|
158
+ [{ users: db.query("SELECT * FROM users") }, 200]
159
+ end
160
+ end
161
+ ```
@@ -0,0 +1,201 @@
1
+ ---
2
+ title: Middleware
3
+ ---
4
+
5
+ # Middleware
6
+
7
+ Middleware wraps your application, processing requests before handlers and responses after.
8
+
9
+ ## Built-in Middleware
10
+
11
+ FunApi provides convenience methods for common middleware:
12
+
13
+ ### CORS
14
+
15
+ Handle Cross-Origin Resource Sharing:
16
+
17
+ ```ruby
18
+ api.add_cors(
19
+ allow_origins: ['http://localhost:3000', 'https://myapp.com'],
20
+ allow_methods: ['GET', 'POST', 'PUT', 'DELETE'],
21
+ allow_headers: ['Content-Type', 'Authorization'],
22
+ expose_headers: ['X-Request-Id'],
23
+ max_age: 600,
24
+ allow_credentials: true
25
+ )
26
+ ```
27
+
28
+ For development, allow all origins:
29
+
30
+ ```ruby
31
+ api.add_cors(allow_origins: ['*'])
32
+ ```
33
+
34
+ ### Request Logger
35
+
36
+ Log incoming requests:
37
+
38
+ ```ruby
39
+ api.add_request_logger
40
+ ```
41
+
42
+ With custom logger:
43
+
44
+ ```ruby
45
+ api.add_request_logger(
46
+ logger: Logger.new('logs/requests.log'),
47
+ level: :info
48
+ )
49
+ ```
50
+
51
+ ### Trusted Host
52
+
53
+ Validate the Host header (security):
54
+
55
+ ```ruby
56
+ api.add_trusted_host(
57
+ allowed_hosts: ['myapp.com', 'api.myapp.com']
58
+ )
59
+ ```
60
+
61
+ With regex patterns:
62
+
63
+ ```ruby
64
+ api.add_trusted_host(
65
+ allowed_hosts: ['localhost', /\.myapp\.com$/]
66
+ )
67
+ ```
68
+
69
+ ### Gzip Compression
70
+
71
+ Compress JSON responses:
72
+
73
+ ```ruby
74
+ api.add_gzip
75
+ ```
76
+
77
+ ## Using Rack Middleware
78
+
79
+ Any Rack middleware works with FunApi:
80
+
81
+ ```ruby
82
+ app = FunApi::App.new do |api|
83
+ api.use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
84
+ api.use Rack::Attack
85
+ api.use Rack::ETag
86
+ end
87
+ ```
88
+
89
+ ## Custom Middleware
90
+
91
+ Create middleware following the Rack pattern:
92
+
93
+ ```ruby
94
+ class TimingMiddleware
95
+ def initialize(app)
96
+ @app = app
97
+ end
98
+
99
+ def call(env)
100
+ start = Time.now
101
+ status, headers, body = @app.call(env)
102
+ duration = Time.now - start
103
+
104
+ headers['X-Response-Time'] = "#{(duration * 1000).round}ms"
105
+ [status, headers, body]
106
+ end
107
+ end
108
+
109
+ app = FunApi::App.new do |api|
110
+ api.use TimingMiddleware
111
+ end
112
+ ```
113
+
114
+ ### With Options
115
+
116
+ ```ruby
117
+ class AuthMiddleware
118
+ def initialize(app, secret:, exclude: [])
119
+ @app = app
120
+ @secret = secret
121
+ @exclude = exclude
122
+ end
123
+
124
+ def call(env)
125
+ path = env['PATH_INFO']
126
+
127
+ if @exclude.include?(path)
128
+ return @app.call(env)
129
+ end
130
+
131
+ token = env['HTTP_AUTHORIZATION']&.delete_prefix('Bearer ')
132
+
133
+ unless valid_token?(token)
134
+ return [401, {'content-type' => 'application/json'}, ['{"error":"Unauthorized"}']]
135
+ end
136
+
137
+ @app.call(env)
138
+ end
139
+
140
+ private
141
+
142
+ def valid_token?(token)
143
+ # Verify token with @secret
144
+ end
145
+ end
146
+
147
+ app = FunApi::App.new do |api|
148
+ api.use AuthMiddleware,
149
+ secret: ENV['JWT_SECRET'],
150
+ exclude: ['/health', '/docs', '/openapi.json']
151
+ end
152
+ ```
153
+
154
+ ## Middleware Order
155
+
156
+ Middleware runs in the order registered (first in, first out):
157
+
158
+ ```ruby
159
+ app = FunApi::App.new do |api|
160
+ api.use LoggingMiddleware # 1. Runs first
161
+ api.use AuthMiddleware # 2. Runs second
162
+ api.use TimingMiddleware # 3. Runs third
163
+
164
+ # Then your routes handle the request
165
+ # Response goes back through in reverse order
166
+ end
167
+ ```
168
+
169
+ Request flow:
170
+ ```
171
+ Request → Logging → Auth → Timing → Handler
172
+ Response ← Logging ← Auth ← Timing ← Handler
173
+ ```
174
+
175
+ ## Complete Example
176
+
177
+ ```ruby
178
+ require 'funapi'
179
+ require 'funapi/server/falcon'
180
+
181
+ app = FunApi::App.new(title: "My API") do |api|
182
+ # Security
183
+ api.add_trusted_host(allowed_hosts: ['localhost', 'myapi.com'])
184
+ api.add_cors(allow_origins: ['https://myapp.com'])
185
+
186
+ # Logging
187
+ api.add_request_logger
188
+
189
+ # Compression
190
+ api.add_gzip
191
+
192
+ # Custom
193
+ api.use Rack::Session::Cookie, secret: 'secret'
194
+
195
+ api.get '/hello' do |input, req, task|
196
+ [{ message: 'Hello!' }, 200]
197
+ end
198
+ end
199
+
200
+ FunApi::Server::Falcon.start(app, port: 3000)
201
+ ```