A Swift version of Retrofit, inspired by Retrofit's API design and enhanced with Swift's type inference capabilities. Automatically recognizes scenarios without extra annotations.
- Response KeyPath Parsing (nested)
- Tuple Returns (nested)
- Request and Response Interceptor
- SSE (Server-Sent Events)
@API
@Headers(["token": "Bearer JWT_TOKEN"])
struct UsersAPI {
@GET("/user")
func getUser(id: String) async throws -> User
// GET /user?id=...
@POST("/user")
func createUser(email: String, password: String) async throws -> (id: String, name: String)
// POST /user (body: {"email": String, "password": String}})
@GET("/users/{username}/todos")
@ResponseKeyPath("data.list")
func getTodos(username: String) async throws -> [Todo]
// GET /users/john/todos
@POST("/chat/completions")
@Headers(["Authorization": "Bearer ..."])
@EventStreaming
func completions(model: String, messages: [Message], stream: Bool = true) async throws -> AsyncStream<String>
// POST /chat/completions (body: {"model": String, "messages": [Message], stream: true}})
}
let provider = Provider(baseURL: "https://www.example.com")
let api = UsersAPI(provider)
let resp = try await api.getUser(id: "john")
for await event in try await api.completions(model: "gpt-5", messages: ...) {
print(event)
}
Swift Package Manager
.package(url: "https://github.com/winddpan/Netrofit", from: "0.1.0")
.product(name: "Netrofit", package: "Netrofit")
Supported HTTP methods:
@GET("/users/list")
func listUsers() async throws -> [User]
// GET /users/list
@POST("/users/new")
func createUser(_ user: User) async throws -> User
// POST /users/new (body: User)
@PUT("/users/{id}")
func updateUser(id: Int, _ user: User) async throws -> User
// PUT /users/{id} (body: User)
@PATCH("/users/{id}")
func partialUpdateUser(id: Int, _ fields: [String: String]) async throws -> User
// PATCH /users/{id} (body: fields)
@DELETE("/users/{id}")
func deleteUser(id: Int) async throws -> Void
// DELETE /users/{id}
@OPTIONS("/meta")
func options() async throws -> MetaInfo
// OPTIONS /meta
@HEAD("/resource/{id}")
func checkResource(id: Int) async throws -> HTTPHeaders
// HEAD /resource/{id}- Parameters with the same name as
{placeholder}in the URL are automatically mapped without annotation - Use
@Pathto explicitly specify when parameter names differ
@GET("/group/{id}/users")
func groupList(id: Int) async throws -> [User]
// GET /group/{id}/users
@GET("/group/{gid}/users")
func groupList(@Path("gid") gid: Int) async throws -> [User]
// GET /group/{gid}/users
@GET("/group/{gid}/users")
func groupList(@Path("gid") groupId: Int) async throws -> [User]
// GET /group/{gid}/users
@GET("/group/{gid}/users")
func groupList(@Path(encoded: true) gid: Int) async throws -> [User]
// GET /group/{gid}/users
@GET("/group/{gid}/users")
func groupList(@Path("gid", encoded: true) groupId: Int) async throws -> [User]
// GET /group/{gid}/users- Simple type parameters are automatically mapped to query parameters (except those matched with @Path)
- Dictionary is automatically expanded to
&key=value - Use
@Queryto override parameter names or for non-GET requests like POST
@GET("/transactions")
func getTransactions(merchant: String) async throws -> [Transaction]
// GET /transactions?merchant=...
@GET("/group/{id}/users")
func groupList(id: Int, sort: String) async throws -> [User]
// GET /group/42/users?sort=...
@GET("/search")
func searchUsers(filters: [String: String]) async throws -> [User]
// GET /search?name=...&age=...
@GET("/search")
func searchUsers(keyword: String) async throws -> [User]
// GET /search?keyword=...
@GET("/search")
func searchUsers(q keyword: String) async throws -> [User]
// GET /search?q=...
@GET("/search")
func searchUsers(@Query("q") keyword: String) async throws -> [User]
// GET /search?q=...
@GET("/search")
func searchUsers(@Query(encoded: true) keyword: String) async throws -> [User]
// GET /search?q=...
@POST("/search")
func searchUsers(@Query("q", encoded: true) keyword: String) async throws -> [User]
// POST /search?q=...- In POST/PUT/PATCH, unnamed parameters (parameter label is
_) are automatically used as Body - Use
@Bodyto explicitly specify or for non-standard requests like GET
@POST("/users/new")
func createUser(_ user: User) async throws -> User
// POST /users/new (body: User)
@POST("/items")
func addItem(item: Item, @Query("notify") notify: Bool) async throws -> Item
// POST /items?notify=true (body: {"item": Item})
@POST("/items")
func addItem(@Body item: Item, @Query("notify") notify: Bool) async throws -> Item
// POST /items?notify=true (body: Item)- In POST/PUT/PATCH, object parameters not used for
@Query/@Path/@Header/@Bodyare automatically used as Body Fields - Use
@Fieldto explicitly specify field names or for non-standard requests like GET
@POST("/users/new")
func createUser(user: User) async throws -> User
// POST /users/new (body: {"user": User})
@POST("/users/new")
func createUser(name: String, id: String) async throws -> User
// POST /users/new (body: {"name": String, "id": String})
@POST("/users/new")
func createUser(@Field("new_name") name: String, id: String) async throws -> User
// POST /users/new (body: {"new_name": String, "id": String})
@POST("/users/new")
@FormUrlEncoded
func createUser(@Field("new_name") name: String, id: String) async throws -> User
// POST /users/new (form body: new_name=...&id=...})- JSON is the default body encoding for
application/json - Supports custom encoder and decoder
@POST("/users/new")
func createUser(_ user: User) async throws -> User
// POST /users/new (json body: User)
@JSON
@POST("/users/new")
func createUser(id: String, name: String) async throws -> User
// POST /users/new (json body: {"id": String, "name": String})
// Supports custom encoder and decoder
@JSON(encoder: JSONEncoder(), decoder: DynamicContentTypeDecoder())
@POST("/data")
func createData(user: User) async throws -> User
// POST /data (json body: {"user": User})For application/x-www-form-urlencoded, supports custom encoder and decoder.
@FormUrlEncoded
@POST("/user/edit")
func updateUser(firstName: String, lastName: String) async throws -> User
// POST /user/edit (form body: firstName=...&lastName=...)
@FormUrlEncoded
@POST("/user/edit")
func updateUser(@Field("first") firstName: String, @Field("last") lastName: String) async throws -> User
// POST /user/edit (form body: first=...&last=...)
// Supports custom encoder and decoder
@FormUrlEncoded(encoder: URLEncodedFormEncoder(), decoder: JSONDecoder())
@POST("/form")
func submitForm(data: FormData) async throws
// POST /form (form body: ...=...&....=...&...=...)For file uploads or rich media content, supports custom encoder and decoder.
@Multipart
@PUT("/user/photo")
func updateUser(
@Part(name: "photo", filename: "avatar.jpg", mimeType: "image/jpeg") photo: Data,
@Part(name: "desc") description: String
) async throws -> User
// PUT /user/photo (multipart: photo, description)
// @Part supports custom name, filename, mimeType
// Supports custom encoder and decoder
@Multipart(encoder: MultipartEncoder(), decoder: JSONDecoder())
@POST("/upload")
func uploadFile(file: URL, meta: [String: String]) async throws -> UploadResponse
// POST /upload (multipart: file,meta)@Headers([
"Cache-Control": "max-age=640000",
"Accept": "application/vnd.github.v3.full+json"
])
@GET("/users/{username}")
func getUser(username: String) async throws -> User
// GET /users/johne@GET("/user")
func getUser(@Header("Authorization") token: String) async throws -> User
// GET /user (header: {"Authorization": ...})
@GET("/user")
func getUser(@HeaderMap headers: [String: String]) async throws -> User
// GET /user (header {...})Use @ResponseKeyPath to parse a KeyPath in JSON, supports multi-level nesting.
@GET("/users")
@ResponseKeyPath("data.list")
func listUsers() async throws -> [User]
// GET /users (response: {"data": {"list": [...]}})Supports tuple return values with nested tuples. Tuple elements map response data in order.
@GET("/user")
func getUser(id: Int) async throws -> (id: String, name: String)
// GET /user?id=...
@GET("/users")
func getUserList() async throws -> (list: [(id: String, name: String)], count: Int)
// GET /users@EventStreamingis for Server-Sent Events continuous streaming scenarios- Returns
AsyncStreamorAsyncThrowingStream, consume events withfor await
@EventStreaming
@GET("/events/stream")
func listenEvents(roomID: String) async throws -> AsyncStream<String>
// GET /events/stream?roomID=... continuous event streaming
@EventStreaming
@GET("/events/stream")
func listenEventsThrowing(roomID: String) async throws -> AsyncThrowingStream<String, Error>
// GET /events/stream?roomID=... continuous event streaming
for await event in try await api.listenEvents(roomID: "chat") {
print("Received event:", event)
}-
Automatic Path Parameter Matching
{placeholder}in URL paths automatically matches parameters with the same name- Only need explicit
@Pathwhen names don't match
-
Automatic Query Parameter Inference
- Except for Path parameters, simple types (String, Int, Bool, etc.) are automatically mapped to query parameters
- Dictionary is automatically expanded to multiple query items
-
Automatic Field Parameter Inference
- In
@FormUrlEncodedmethods, basic type parameters are automatically mapped to form fields - Parameter name is used as field name unless
@Fieldspecifies an alias - In POST/PUT/PATCH, object parameters not used for
@Query/@Path/@Header/@Bodyare automatically used as Body Fields
- In
-
Automatic Body Parameter Inference
- In POST/PUT/PATCH, unnamed parameters (parameter label is
_) are automatically used as Body
- In POST/PUT/PATCH, unnamed parameters (parameter label is
-
Default Encoding Rules
- JSON (
application/json) is the default body encoding - URL Encoding is the default query parameter encoding
- JSON (
- TODO: Async & Combine: Supports
async/awaitandPublisher - Global Interceptors: Supports registering header, logging, and auth interceptors