Skip to content

Commit 2742d15

Browse files
committed
Add a recommendations document
1 parent f69a0d9 commit 2742d15

File tree

2 files changed

+343
-0
lines changed

2 files changed

+343
-0
lines changed
Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
## Dependencies
2+
3+
Hummingbird won't enforce any dependencies, or provide special treatment for dependencies on a framework level. However, we'll provide some guidelines and tips for _common_ dependencies and how to use them.
4+
5+
## Project Structure
6+
7+
The project structure is a recommended project layout, defining what modules are recommended to create and how these modules are structured in terms of folders and files.
8+
9+
### Module Structure
10+
11+
Every Hummingbird projects neds a primary executable target. This is the **MyApp** target in the example below.
12+
13+
If a user has an OpenAPI specification for their API, a separate OpenAPI module is recommended based on [swift-openapi-generator](https://github.com/apple/swift-openapi-generator). This module will be used to generate the API server code.
14+
15+
```
16+
├── Sources
17+
│ ├── MyApp
18+
│ └── MyAppOpenAPI # Optional
19+
20+
├── Tests
21+
│ └── MyAppTests
22+
23+
└── Package.swift
24+
```
25+
26+
## App Module Structure
27+
28+
The **MyApp** module is the primary module for the project. It contains the entry point for the application in **App.swift**, and has folders for **routes**, **database**, **repositories**, **services**, **middleware** and **config**.
29+
30+
31+
```
32+
├── Routes
33+
│ ├── ...Routes.swift
34+
│ └── BuildRouter.swift
35+
36+
├── Database
37+
│ ├── Models
38+
│ └── Migrations
39+
40+
├── Repositories
41+
│ └── UserRepository.swift
42+
43+
├── Services
44+
│ └── OTelService.swift
45+
│ └── DatabaseService.swift
46+
47+
├── Middleware
48+
│ └── AuthMiddleware.swift
49+
50+
├── Application+build.swift
51+
└── App.swift
52+
```
53+
54+
### App.swift
55+
56+
The **App.swift** file is the entry point for the application. It's a command line tool based on [swift-argument-parser](https://github.com/apple/swift-argument-parser), allowing deployments to customize the application's configuration through command line arguments.
57+
58+
The `MyApp` type also confirms to `MyAppArguments`, which is a user-defined protocol that specifies the command line arguments for the application. This enables the Hummingbird Application to be configured with different settings when running in a unit test, or other environments.
59+
60+
```swift
61+
import ArgumentParser
62+
import Hummingbird
63+
64+
@main
65+
struct MyApp: AsyncParsableCommand, MyAppArguments {
66+
@Option(name: .shortAndLong)
67+
var httpHostname: String = "127.0.0.1"
68+
69+
@Option(name: .shortAndLong)
70+
var httpPort: Int = 8080
71+
72+
/// The connection string for the PostgreSQL database
73+
@Option(name: .shortAndLong)
74+
var postgres: String = "postgres://localhost:5432/myapp"
75+
76+
func run() async throws {
77+
let app = try await buildApplication(self)
78+
try await app.runService()
79+
}
80+
}
81+
```
82+
83+
### Application+Build.swift
84+
85+
The **Application+build.swift** file is a recommended file that contains the `buildApplication` function. This function is used to build the application, and is used in the `App.swift` file.
86+
87+
The file also contains a global default `RequestContext` type, which is used to customize the context carried through the request lifecycle for the application. The default `typealias` enables users to easily customize the type without having to change the entire application.
88+
89+
```swift
90+
// Customize the RequestContext type to your needs
91+
typealias AppRequestContext = BasicRequestContext
92+
93+
protocol MyAppArguments {
94+
var httpHostname: String { get }
95+
var httpPort: Int { get }
96+
var postgres: String { get }
97+
}
98+
99+
func buildApplication(_ arguments: some MyAppArguments) async throws -> Application {
100+
let router = Router(context: AppRequestContext.self)
101+
router.add("users", routes: UserController().routes)
102+
let logger = Logger(label: "MyApp")
103+
104+
let app = Application(
105+
router: router,
106+
configuration: .init(
107+
address: .hostname(arguments.httpHostname, port: arguments.httpPort),
108+
serverName: "MyApp"
109+
),
110+
logger: logger
111+
)
112+
app.addServices(PostgresService(connectionString: arguments.postgres))
113+
return app
114+
}
115+
```
116+
117+
## Routes and Controllers
118+
119+
The **routes** folder contains functions that apply routes to the router through "controller" types. Each Controller is generically contrained over the ``RequestContext`` type, so that it can be used with any Router, RouterGroup or other implementation.
120+
121+
Let's take **UserController.swift** as an example:
122+
123+
```swift
124+
struct UserController<Context: RequestContext> {
125+
var routes: RouteCollection<Context> {
126+
return RouteCollection(context: Context.self)
127+
.get { request, context in
128+
...
129+
}
130+
}
131+
}
132+
```
133+
134+
Controllers can specify dependencies on services they need, like a database connection. To do so, add these properties on the controller:
135+
136+
```swift
137+
struct UserController<Context: RequestContext> {
138+
// Dependency on MongoDB
139+
let database: MongoDatabase
140+
141+
var routes: RouteCollection<Context> {
142+
return RouteCollection(context: Context.self)
143+
.get { request, context in
144+
...
145+
}
146+
}
147+
}
148+
```
149+
150+
If you need the ability to inject dependencies, you can use a `protocol` to abstract the implementation of the dependency.
151+
152+
```swift
153+
protocol SomeInjectableService {
154+
func doSomething()
155+
}
156+
157+
struct UserController<Context: RequestContext> {
158+
let service: any SomeInjectableService
159+
160+
var routes: RouteCollection<Context> {
161+
return RouteCollection(context: Context.self)
162+
.get { request, context in
163+
self.service.doSomething()
164+
...
165+
}
166+
}
167+
}
168+
```
169+
170+
All Controllers' routes are combined into a single router in **BuildRouter.swift**:
171+
172+
```swift
173+
let router = Router(context: MyContext.self)
174+
router.add("users", routes: UserController().routes)
175+
```
176+
177+
## Request Context
178+
179+
It's also recommended to users that they refine the AppContext requirements for only as much as strictly required by the routes. For example:
180+
181+
```swift
182+
// ✅ This is recommended, because it allows a different RequestContext to be used for login routes
183+
struct UserController<Context: RequestContext> { ... }
184+
```
185+
186+
If a route has requirements for the request, like being authenticated, it's recommended to leverage _protocol composition_ to refine the RequestContext.
187+
188+
```swift
189+
// ✅ This is recommended, because it leverages protocol composition to refine the RequestContext
190+
// while still being flexible enough to allow different concrete types to fulfil the requirements
191+
struct UserProfileController<
192+
Context: RequestContext & AuthenticatedRequestContext
193+
> { ... }
194+
```
195+
196+
Users should define various traits related to a request's state (context) in separate protocols.
197+
198+
```swift
199+
protocol RateLimitedRequestContext {
200+
var rateLimitStatus: RateLimitStatus { get }
201+
var ipAddress: String { get }
202+
}
203+
204+
protocol AuthenticatedRequestContext {
205+
var user: User { get }
206+
}
207+
```
208+
209+
## Middleware
210+
211+
Middleware follow the same RequestContext conventions and recommendations as Routes. There's one distinction between Routes in that middleware can modify the request context.
212+
213+
```swift
214+
public struct AuthenticationGuardMiddleware<Context: AuthRequestContext>: RouterMiddleware {
215+
public init() {}
216+
217+
public func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
218+
guard context.identity != nil else {
219+
throw HTTPError(.unauthorized)
220+
}
221+
return try await next(request, context)
222+
}
223+
}
224+
```
225+
226+
### Child Request Contexts
227+
228+
```swift
229+
struct MyAppRequestContext: RequestContext {
230+
public var coreContext: CoreRequestContextStorage
231+
// User is optional, because it's not required for all routes
232+
public var user: User?
233+
234+
public init(source: Source) {
235+
self.coreContext = .init(source: source)
236+
self.routerContext = .init()
237+
self.user = nil
238+
}
239+
}
240+
```
241+
242+
```swift
243+
struct AuthenticatedRequestContext: ChildRequestContext {
244+
public var coreContext: CoreRequestContextStorage
245+
// User is required in all routes, and this is statically known
246+
public var user: User
247+
248+
init(context: MyAppRequestContext) throws {
249+
guard let user = context.user else {
250+
throw Abort(.unauthorized)
251+
}
252+
253+
self.coreContext = context.coreContext
254+
self.user = user
255+
}
256+
}
257+
```
258+
259+
## Services
260+
261+
Users should define (external) resources in `Services`. For example:
262+
263+
- PostgreSQL
264+
- S3 Bucket Management
265+
- Job Queues
266+
- Caching
267+
- OpenTelemetry
268+
- File System Access
269+
270+
Each service manages it's lifecycle through [swift-service-lifecycle](https://github.com/swift-server/swift-service-lifecycle). The order of services is important, as services are initialized in the order they are added to the application, and teared down in reverse order.
271+
272+
```swift
273+
let app = Application()
274+
let postgres = PostgresService(connectionString: arguments.postgres)
275+
app.addServices(
276+
OpenTelemetryService(),
277+
// Postgres reports to OTel through the global logger, so initialized later
278+
postgres,
279+
// Depends on postgres, so initialized later
280+
PostgresJobQueueService(postgres: postgres)
281+
)
282+
```
283+
284+
## Observability
285+
286+
Swift's Observability APIs offer a powerful and flexible way to instrument and observe the application. Hummingbird already reports to [swift-log](https://github.com/apple/swift-log) and provides middleware for [swift-metrics](https://github.com/apple/swift-metrics) and [swift-distributed-tracing](https://github.com/apple/swift-distributed-tracing). These will report to the globally set up observability backend(s) automatically, if they're set up.
287+
288+
By default, logs are emitted to `stdout`, and metrics + traces are discarded. If you want to trace signals in a single place, we recommend using [swift-otel](https://github.com/swift-otel/swift-otel/issues) as an observability backend.
289+
290+
See the [OTel Example Project](https://github.com/hummingbird-project/hummingbird-examples/tree/main/open-telemetry) for a complete example.
291+
292+
### Logging Recommendations
293+
294+
Use the [Logging Guidelines](https://www.swift.org/documentation/server/guides/libraries/log-levels.html) set forth by the Swift Server Workgroup. It's strongly recommended to leverage **structured logging** to make logs more readable and searchable.
295+
296+
✅ Good:
297+
298+
```swift
299+
logger.info("User logged in", metadata: ["user_id": .string(user.id)])
300+
```
301+
302+
❌ Bad:
303+
304+
```swift
305+
logger.info("User \(user.id) logged in")
306+
```
307+
308+
## Scalability
309+
310+
Hummingbird is explicitly designed for resource efficiency. A good example is how HTTP bodies are always represented as an `AsyncSequence` of bytes. This means that the server can stream data to the client without having to buffer the entire response in memory. Likewise, any uploads from a user through Hummingbird can be efficiently handled through the `AsyncSequence` APIs.
311+
312+
It's highly recommended that you avoid `collect`ing responses into memory if possible. Instead, attempt to leverage the `AsyncSequence` APIs to stream data.
313+
314+
Any data that needs to be stored in contiguous memory, like a JSON blob, should be strictly limited in the maximum size as to avoid (memory) resource exhaustion and performance issues.
315+
316+
### Handling Bodies
317+
318+
If you're using standard libraries like Foundation's `JSONDecoder`, limit the body size to "reasonable" sizes. What is considered reasonable depends per application, but lower is generally better.
319+
320+
[MultipartKit 5](https://github.com/vapor/multipart-kit) will support streaming multipart parsing, in addition to [IkigaJSON](https://github.com/orlandos-nl/ikigajson) for streaming JSON parsing.
321+
322+
You can handle the request/response bodies as an `AsyncSequence`, so you can iterate over the body. This will provide backpressure to the source if any part of the system cannot keep up.
323+
324+
```swift
325+
router.get { req, context in
326+
for try await chunk in req.body {
327+
// Do something with the chunk, like writing to the filesystem
328+
}
329+
330+
return Response(status: .ok)
331+
}
332+
```
333+
334+
In the above example, if the disk cannot keep up, the client's upload speed will be throttled to match the disk's speed. This prevents excessive memory build up in the server.
335+
336+
### Persistence
337+
338+
Leverage [swift-jobs](https://github.com/hummingbird-project/swift-jobs) or other job queue implementations to offload long running tasks, or tasks that can be parallelized to a background job. This will enable you to scale your application horizontally by adding more instances of your Hummingbird app.
339+
340+
Avoid local database systems such as [SQLite](https://sqlite.org), including wrappers like [GRDB](https://github.com/groue/GRDB.swift), as these only reside on the local machine.
341+
342+
Database such as [Postgres](https://www.postgresql.org) or [Valkey](https://valkey.io/)/[Redis](https://redis.io) are highly scalable and mature solutions for persistence and/or caching. These databases have mature libraries for Swift, and are well supported by the community.

Hummingbird.docc/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Below is a list of guides and tutorials to help you get started with building yo
3434

3535
- <doc:GettingStarted>
3636
- <doc:Todos>
37+
- <doc:Recommendations>
3738

3839
### Hummingbird Server
3940

0 commit comments

Comments
 (0)