Make HTTP requests in a clear and type safe way by declaring web services and endpoints on iOS, macOS, and Linux
When it comes to making URL requests with Swift, you largely have two options: use the URLSession APIs in Foundation or use some heavy handed framework.
This framework is designed to be light-weight while remaining customizable and focusing on declaring the interface to an API in a declarative manner. Once declared, making requests to the various endpoints is very straight-forward and type safe. It works on iOS, macOS, and Linux.
Andrew developed this strategy through the implementation of many different apps and backend services written in Swift. He's used this paradigm for communicating between his own front and back-ends (both implemented in Swift) as well as to services such as Spotify, FreshDesk, Stripe, and more.
We offer a separate repository DecreeServices with service declarations for popular services
Four types of Endpoints
These protocols declare if an endpoint has input and/or output.
EmptyEndpoint
(no input or output)InEndpoint
(only input)OutEndpoint
(only output)InOutEndpoint
(input and output)
Five Input formats
These formats are used to encode the endpoint's input using the Swift Encodable protocol.
- JSON
- URL Query
- Form URL Encoded
- Form Data Encoded
- XML
Two Output formats
These formats are used to initialize the endpoint's output using the Swift Decodable protocol.
- JSON
- XML
Three types of Authorization
Allows setting authorization to be used for all endpoints in a web service. Each endpoint can then specify an authorization requirement.
Advanced Functionality
- Download result to a file for to save memory on large requests.
- Optionally get progress updates through an onProgress handler
Configurable
You can optionally perform advanced configuration to the processing of a request and response.
- Customize URLRequest (e.g. custom headers)
- Customize JSON encoders and decoders
- Custom response validation
- Custom error response format
- Custom standard response format
Virtually 100% Code Coverage
The vast majority of our code is covered by unit tests to ensure reliability.
Request and Response Logging
- Option to enable logging out of requests and response for debugging
- Option to specify filter to only log particular endpoints
Thorough Error Reporting
The errors thrown and returned by Decree are designed to be user friendly while also exposing detailed diagnostic information.
Mocking
Allows mocking endpoint responses for easy automated testing.
Third-Party Services
We created a separate framework that defines services and endpoints for several third-party services. Check it out at DecreeServices.
Here are a few examples of how this framework is used.
Here we define a CheckStatus
endpoint that is a GET (the default) with no input or output that exists at the path “/status”.
Scroll down to see the definition of ExampleService.
struct CheckStatus: EmptyEndpoint {
typealias Service = ExampleService
let path = "status"
}
We can then use that definition to make asynchronous requests.
CheckStatus().makeRequest() { result in
switch result {
case .success:
print("Success :)")
case .failure(let error):
print("Error :( \(error)")
}
}
We can also make synchronous requests that simply throw an error if an error occurs.
try CheckStatus().makeSynchronousRequest()
We can also define endpoints that have input and/or output. Here, we define a Login endpoint that is a POST to “/login” with username and password parameters encoded as JSON. If successful, the endpoint is expected to return a token.
struct Login: InOutEndpoint {
typealias Service = ExampleService
static let method = Method.post
let path = "login"
struct Input: Encodable {
let username: String
let password: String
}
struct Output: Decodable {
let token: String
}
}
Then we can make an asynchronous request.
Login().makeRequest(with: .init(username: "username", password: "secret")) { result in
switch result {
case .success(let output):
print("Token: \(output.token)")
case .failure(let error):
print("Error :( \(error)")
}
}
Or we can make a synchronous requests that returns the output if successful and throws otherwise.
let token = try Login().makeSynchronousRequest(with: .init(username: "username", password: "secret")).token
For endpoints with larger output, we can download them directly to a file instead of holding the whole response in memory.
struct GetDocument: OutEndpoint {
typealias Service = ExampleService
typealias Output = Data
let id: Int
var path: String {
return "documents/\(id)"
}
}
GetDocument(id: 42).makeDownloadRequest() { result in
switch result {
case .success(let url):
// open or move the url
case .failure(let error):
print("Error :( \(error)")
}
}
Note, you use a the makeDownloadRequest
method on any endpoint with output (regardless of it's format), but it often makes most sense with raw data output.
The only extra code necessary to make the above examples work, is to define the ExampleService:
struct ExampleService: WebService {
// There is no service wide standard response format
typealias BasicResponse = NoBasicResponse
// Errors will be in the format {"message": "<reason>"}
struct ErrorResponse: AnyErrorResponse {
let message: String
}
// Requests should use this service instance by default
static var shared = ExampleService()
// All requests will be sent to their endpoint at "https://example.com"
let baseURL = URL(string: "https://example.com")!
}
Here we define a WebService
called ExampleService
with the a few properties.
That's all you need. You can then define as many endpoints as you like and use them in a clear and type safe way.
To see real world examples, check out how we declared services in DecreeServices.
Features we are hoping to implement are added as issues with the enhancement tag. If you have any feature requests, please don't hesitate to create a new issue.
It is very much encouraged for you to report any issues and/or make pull requests for new functionality.