A modern, type-safe networking library for Swift.
Read the full documentation here.
Add the following dependency to your Package.swift file:
dependencies: [
.package(url: "https://github.com/bpisano/network-kit", .upToNextMajor(from: "1.2.0"))
]NetworkKit distinguishes between clients and requests. This separation allows you to use the same requests across different server environments (dev, staging, prod), configure each environment with its own base URL and settings, and keep your request logic environment-agnostic.
A client represents a server environment with its base URL and configuration:
let client = Client("https://api.example.com")Requests define the API endpoints and parameters. They're reusable across all environments:
@Get("/users/:id")
@Response(User.self)
struct GetUserRequest {
@Path
let id: String
@Query
let includePosts: Bool
}let request = GetUserRequest(
id: "123"
includePosts: true
)
let response = try await client.perform(request)
let user = try response.decodedDataRequests define the API endpoints and parameters. They're reusable across all environments and can be configured with various HTTP methods, path parameters, query parameters, and request bodies.
NetworkKit comes with several macros to simplify and streamline request declaration.
@Get("/users")
struct GetUsersRequest {
@Query
let page: Int
@Query
let limit: Int
}Generated request:
GET https://api.example.com/users?page=1&limit=20Click to see all HTTP methods
@Post("/users")
struct CreateUserRequest {
@Body
struct Body: HttpBody {
let name: String
let email: String
}
}Click to see the generated request
POST https://api.example.com/users
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com"
}@Put("/users/:id")
struct UpdateUserRequest {
@Path
let id: String
@Body
struct Body: HttpBody {
let name: String
let email: String
}
}Click to see the generated request
PUT https://api.example.com/users/123
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com"
}@Delete("/users/:id")
struct DeleteUserRequest {
@Path
let id: String
}Click to see the generated request
DELETE https://api.example.com/users/123@Patch("/users/:id")
struct PatchUserRequest {
@Path
let id: String
@Body
struct Body: HttpBody {
let name: String?
let email: String?
}
}Click to see the generated request
PATCH https://api.example.com/users/123
Content-Type: application/json
{
"name": "Updated Name"
}@Head("/users/:id")
struct CheckUserRequest {
@Path
let id: String
}Click to see the generated request
HEAD https://api.example.com/users/123@Options("/users")
struct OptionsUserRequest {
}Click to see the generated request
OPTIONS https://api.example.com/users@Connect("/proxy")
struct ConnectProxyRequest {
@Query
let host: String
@Query
let port: Int
}Click to see the generated request
CONNECT https://api.example.com/proxy?host=example.com&port=80@Trace("/debug")
struct TraceRequest {
}Click to see the generated request
TRACE https://api.example.com/debugThe @Response macro allows you to define the type of the response your request expects. This enables NetworkKit to automatically decode the response into the specified type.
@Get("/users/:id")
@Response(User.self)
struct GetUserRequest {
@Path
let id: String
}Make sure that the type you specify conforms to
Decodable.
You can also define your response type directly inside your request:
@Get("/users/:id")
struct GetUserRequest {
@Response
struct UserDto {
let id: String
let name: String
}
@Path
let id: String
}When used inside a request, don't specify any arguments to the
@Responsemacro.
When used this way, the @Response macro will automatically add the Decodable conformance to your struct.
Use @Path for URL path parameters. These are replaced in the URL path at runtime:
@Get("/users/:id/posts/:postId")
struct GetPostRequest {
@Path
let id: String
@Path
let postId: String
}The path in the macro should use a colon (e.g.,
:id) to indicate a path parameter, and the corresponding Swift property name in your struct must match the parameter name. For example, if your macro is@Get("/users/:id/posts/:postId"), your struct should have properties namedidandpostId.
Path parameters work with any type that conforms to PathRepresentable. NetworkKit provides built-in support for:
- String types:
String,Substring,Character - Integer types:
Int,Int8,Int16,Int32,Int64,UInt,UInt8,UInt16,UInt32,UInt64 - Floating point types:
Float,Double,Decimal - Boolean type:
Bool - Foundation types:
UUID,Date,URL,NSNumber - Arrays:
[Element]whereElement: PathRepresentable
You can also specify custom path parameter names:
@Get("/users/:userId/posts/:postId")
struct GetPostRequest {
@Path("userId")
let userIdentifier: String
@Path("postId")
let documentId: String
}Click to see the generated request
GET https://api.example.com/users/123/posts/456Use @Query for URL query parameters. These are automatically added to the URL:
@Get("/search")
struct SearchRequest {
@Query
let query: String
@Query
let page: Int
@Query
let limit: Int
}Click to see the generated request
GET https://api.example.com/search?query=swift&page=1&limit=20You can also provide the name of the query parameter explicitly:
@Get("/search")
struct SearchRequest {
@Query("q")
let query: String
}Click to see the generated request
GET https://api.example.com/search?q=swiftQuery parameters work with any type that conforms to
QueryRepresentable. NetworkKit provides built-in support for:
- String types:
String,Substring,Character - Integer types:
Int,Int8,Int16,Int32,Int64,UInt,UInt8,UInt16,UInt32,UInt64 - Floating point types:
Float,Double,Decimal - Boolean type:
Bool(converted to "true"/"false") - Foundation types:
UUID,Date,URL,NSNumber - Arrays:
[Element]whereElement: QueryRepresentable(elements joined with commas)
NetworkKit automatically handles HTTP body serialization for your requests. Simply include a body property in your request struct, and it will be serialized according to the HttpBody protocol implementation.
The HttpBody protocol defines how your data should be serialized for HTTP requests. NetworkKit provides default implementations for common types (which are serialized as JSON) and Data (which are sent as binary data).
For Encodable types, NetworkKit automatically serializes the body as JSON. You can use the @Body macro to define your request body:
@Post("/users")
struct CreateUserRequest {
@Body
struct Body {
let name: String
let email: String
}
}Click to see the generated request
POST https://api.example.com/users
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com",
"age": 30
}For binary data, you can use Data directly as the body type:
@Post("/upload")
struct UploadDataRequest {
let body: Data
}Click to see the generated request
POST https://api.example.com/upload
Content-Type: application/octet-stream
[Binary data]NetworkKit supports multipart form data for file uploads. You can mix files and text fields in a single request:
@Post("/upload")
struct UploadRequest {
let imageData: Data
let description: String
var body: some HttpBody {
MultipartForm {
DataField(
"file",
data: imageData,
mimeType: .jpegImage,
fileName: "photo.jpg"
)
TextField(
"description",
value: description
)
}
}
}Click to see the generated request
POST https://api.example.com/upload
Content-Type: multipart/form-data; boundary=networkkit
--networkkit
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: image/jpeg
[Binary image data]
--networkkit
Content-Disposition: form-data; name="description"
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: 8bit
My vacation photo
--networkkit--You can also create custom body types that conform to HttpBody:
struct CustomBody: HttpBody {
let content: String
func modify(_ request: inout URLRequest, using encoder: JSONEncoder) throws {
let data = content.data(using: .utf8) ?? Data()
request.httpBody = data
request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
request.setValue("\(data.count)", forHTTPHeaderField: "Content-Length")
}
}
@Post("/custom")
struct CustomRequest {
let body: CustomBody
}Click to see the generated request
POST https://api.example.com/custom
Content-Type: text/plain
Hello, World!You can define headers for your requests using the headers property. This allows you to specify static headers or dynamic headers based on request properties. For dynamically inserting headers for each request, see the Middleware section.
@Get("/me")
struct GetMeRequest {
let headers: [String: String?] = [
"Authorization": "Bearer your-token"
]
}A client represents a server environment with its base URL and configuration. Clients handle the request execution, middleware processing, and response handling.
When you call perform on a client, the request goes through several well-defined stages:
- Middleware Processing: Before the request is sent, it is passed through a chain of middleware. Each middleware can modify the request (for example, to add authentication headers, logging, or custom logic).
- Sending the Request: After all middleware have been applied, the request is sent to the server.
- Interceptor Processing: Once a response is received, it is passed through a chain of interceptors. Interceptors can inspect or modify the response, handle errors, or implement retry logic.
- Completion: After all interceptors have run, the final result is returned to the user.
NetworkKit provides several methods for performing requests, each suited for different use cases:
For requests that return structured data, use the perform method with a decodable type:
let response: Response<User> = try await client.perform(request)
let user = try response.decodedDataFor requests that don't return a response body (like DELETE requests):
try await client.perform(deleteRequest)For requests where you need to handle the response data manually:
let response: Response<Data> = try await client.performRaw(request)
let rawData = response.dataAll perform methods support progress tracking for large requests:
let response = try await client.perform(request) { progress in
print("Progress: \(progress.fractionCompleted)")
}Middleware allows you to modify requests before they are sent. This is useful for adding authentication headers, logging, or transforming request data.
struct AuthMiddleware: Middleware {
let token: String
func modify(request: inout URLRequest) async throws {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
}
var client = Client("https://api.example.com")
client.middlewares = [AuthMiddleware(token: "your-token")]Interceptors allow you to modify responses after they are received but before they are processed. This is useful for error handling, response transformation, or retry logic.
struct RetryInterceptor: Interceptor {
func intercept(
data: Data,
response: URLResponse,
client: HttpClient,
request: some HttpRequest
) async throws -> (data: Data, response: URLResponse) {
guard let response = response as? HTTPURLResponse else { return (data, response) }
guard response.statusCode == 403 else { return (data, response) }
// Add retry logic here
}
}
var client = Client("https://api.example.com")
client.interceptors = [RetryInterceptor()]NetworkKit includes built-in logging, but you can create custom loggers to integrate with your preferred logging system:
struct CustomLogger: ClientLogger {
func logPerform(request: URLRequest) {
print("π \(request.httpMethod ?? "GET") \(request.url?.absoluteString ?? "")")
}
func logResponse(request: URLRequest, response: URLResponse, data: Data) {
if let httpResponse = response as? HTTPURLResponse {
print("π₯ \(httpResponse.statusCode)")
}
}
}
var client = Client("https://api.example.com")
client.logger = CustomLogger()Click to see the log output
π GET https://api.example.com/users/123
π₯ 200
NetworkKit is released under the MIT License.
