{"id":32151831,"url":"https://github.com/joshuawright11/papyrus","last_synced_at":"2025-10-21T10:53:40.444Z","repository":{"id":45104613,"uuid":"339504840","full_name":"joshuawright11/papyrus","owner":"joshuawright11","description":"A type-safe HTTP client for Swift.","archived":false,"fork":false,"pushed_at":"2024-12-08T01:20:09.000Z","size":312,"stargazers_count":320,"open_issues_count":20,"forks_count":33,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-10-21T10:53:30.868Z","etag":null,"topics":["http","macros","networking","request","response","rest","swift","swift-package-manager"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joshuawright11.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-02-16T19:18:09.000Z","updated_at":"2025-10-02T07:30:28.000Z","dependencies_parsed_at":"2023-09-28T04:26:13.911Z","dependency_job_id":"8f1ead37-5db9-4849-a1aa-611736b5b19b","html_url":"https://github.com/joshuawright11/papyrus","commit_stats":null,"previous_names":["joshuawright11/papyrus"],"tags_count":33,"template":false,"template_full_name":null,"purl":"pkg:github/joshuawright11/papyrus","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshuawright11%2Fpapyrus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshuawright11%2Fpapyrus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshuawright11%2Fpapyrus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshuawright11%2Fpapyrus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joshuawright11","download_url":"https://codeload.github.com/joshuawright11/papyrus/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshuawright11%2Fpapyrus/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":280248569,"owners_count":26297925,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-21T02:00:06.614Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["http","macros","networking","request","response","rest","swift","swift-package-manager"],"created_at":"2025-10-21T10:53:38.977Z","updated_at":"2025-10-21T10:53:40.438Z","avatar_url":"https://github.com/joshuawright11.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 📜 Papyrus\n\n\u003ca href=\"https://swift.org\"\u003e\u003cimg src=\"https://img.shields.io/badge/Swift-5.9-orange.svg\" alt=\"Swift Version\"\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/joshuawright11/papyrus/releases\"\u003e\u003cimg src=\"https://img.shields.io/github/release/joshuawright11/papyrus.svg\" alt=\"Latest Release\"\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/joshuawright11/papyrus/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/github/license/joshuawright11/papyrus.svg\" alt=\"License\"\u003e\u003c/a\u003e\n\nPapyrus is a type-safe HTTP client for Swift.\n\nIt reduces your network boilerplate by turning turns your APIs into clean and concise Swift protocols.\n\nIt's [Retrofit](https://github.com/square/retrofit) for Swift!\n\n```swift\n@API\n@Authorization(.bearer(\"\u003cmy-auth-token\u003e\"))\nprotocol Users {\n    @GET(\"/user\")\n    func getUser() async throws -\u003e User\n\n    @POST(\"/user\")\n    func createUser(email: String, password: String) async throws -\u003e User\n\n    @GET(\"/users/:username/todos\")\n    func getTodos(username: String) async throws -\u003e [Todo]\n}\n```\n\n```swift\nlet provider = Provider(baseURL: \"https://api.example.com/\")\nlet users: Users = UsersAPI(provider: provider)\nlet todos = try await users.getTodos(username: \"joshuawright11\")\n```\n\nEach endpoint of your API is represented as function on the protocol.\n\nAnnotations on the protocol, functions, and parameters help construct requests and decode responses.\n\n## Table of Contents\n\n0. [Features](#features)\n1. [Getting Started](#getting-started)\n2. [Requests](#requests)\n3. [Responses](#responses)\n4. [Advanced](#advanced)\n5. [Testing](#testing)\n6. [Acknowledgements](#acknowledgements)\n7. [License](#license)\n\n## Features\n\n-   [x] Turn REST APIs into Swift Protocols\n-   [x] `async`/`await` _or_ [Callback APIs](#callback-apis)\n-   [x] JSON, URLForm and Multipart Encoding Support\n-   [x] Automatic Key Mapping\n-   [x] Sensible Parameter Defaults Based on HTTP Verb\n-   [x] Automatically Decode Responses with `Codable`\n-   [x] Custom Interceptors \u0026 Request Builders\n-   [x] Advanced Error Handling\n-   [x] Automatic Mocks for Testing\n-   [x] Powered by `URLSession` or [Alamofire](https://github.com/Alamofire/Alamofire) Out of the Box\n-   [x] Linux / Swift on Server Support Powered by [async-http-client](https://github.com/swift-server/async-http-client)\n\n## Getting Started\n\n### Requirements\n\nSupports iOS 13+ / macOS 10.15+.\n\nKeep in mind that Papyrus uses [macros](https://developer.apple.com/documentation/swift/macros) which require Swift 5.9 / Xcode 15 to compile.\n\n### Installation\n\nInstall Papyrus using the [Swift Package Manager](https://www.swift.org/package-manager/), choosing a backing networking library from below.\n\n\u003cdetails\u003e\n  \u003csummary\u003eURLSession\u003c/summary\u003e\n\n### URLSession\n\nOut of the box, Papyrus is powered by `URLSession`.\n\n```swift\n.package(url: \"https://github.com/joshuawright11/papyrus.git\", from: \"0.6.0\")\n```\n\n```swift\n.product(name: \"Papyrus\", package: \"papyrus\")\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eAlamofire\u003c/summary\u003e\n\n### Alamofire\n\nIf you'd prefer to use [Alamofire](https://github.com/Alamofire/Alamofire), use the `PapyrusAlamofire` product.\n\n```swift\n.package(url: \"https://github.com/joshuawright11/papyrus.git\", from: \"0.6.0\")\n```\n\n```swift\n.product(name: \"PapyrusAlamofire\", package: \"papyrus\")\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eAsyncHTTPClient (Linux)\u003c/summary\u003e\n\n### AsyncHTTPClient (Linux)\n\nIf you're using Linux / Swift on Server, use the separate package [PapyrusAsyncHTTPClient](https://github.com/joshuawright11/papyrus-async-http-client). It's driven by the [swift-nio](https://github.com/apple/swift-nio) backed [async-http-client](https://github.com/swift-server/async-http-client).\n\n```swift\n.package(url: \"https://github.com/joshuawright11/papyrus-async-http-client.git\", from: \"0.2.0\")\n```\n\n```swift\n.product(name: \"PapyrusAsyncHTTPClient\", package: \"papyrus-async-http-client\")\n```\n\n\u003c/details\u003e\n\n## Requests\n\nYou'll represent each of your REST APIs with a _protocol_.\n\nIndividual endpoints are represented by a _function_ on that protocol.\n\nThe function's _parameters_ help Papyrus build the request and the _return type_ indicates how to handle the response.\n\n### Method and Path\n\nSet the request method and path as an attribute on the function. Available methods are `GET`, `POST`, `PATCH`, `DELETE`, `PUT`, `OPTIONS`, `HEAD`, `TRACE`, and `CONNECT`. Use `@HTTP(_ path:method:)` if you need a custom method.\n\n```swift\n@POST(\"/accounts/transfers\")\n```\n\n### Path Parameters\n\nParameters in the path, marked with a leading `:`, will be automatically replaced by matching parameters in the function.\n\n```swift\n@GET(\"/users/:username/repos/:id\")\nfunc getRepository(username: String, id: Int) async throws -\u003e [Repository]\n```\n\n#### Query Parameters\n\nFunction parameters on a `@GET`, `@HEAD`, or `@DELETE` request are inferred to be a query.\n\n```swift\n@GET(\"/transactions\") // GET /transactions?merchant=...\nfunc getTransactions(merchant: String) async throws -\u003e [Transaction]\n```\n\nIf you need to add query paramters to requests of other HTTP Verbs, mark the parameter with `Query\u003cT\u003e`.\n\n```swift\n@POST(\"/cards\") // POST /cards?username=...\nfunc fetchCards(username: Query\u003cString\u003e) async throws -\u003e [Card]\n```\n\n### Static Query Parameters\n\nStatic queries can be set directly in the path string.\n\n```swift\n@GET(\"/transactions?merchant=Apple\")\n```\n\n### Headers\n\nA variable request header can be set with the `Header\u003cT\u003e` type. Its key will be automatically mapped to Capital-Kebab-Case. e.g. `Custom-Header` in the following endpoint.\n\n```swift\n@GET(\"/accounts\")\nfunc getRepository(customHeader: Header\u003cString\u003e) async throws\n```\n\n#### Static Headers\n\nYou can set static headers on a request using `@Headers` at the function or protocol scope.\n\n```swift\n@Headers([\"Cache-Control\": \"max-age=86400\"])\n@GET(\"/user\")\nfunc getUser() async throws -\u003e User\n```\n\n```swift\n@API\n@Headers([\"X-Client-Version\": \"1.2.3\"])\nprotocol Users { ... }\n```\n\n#### Authorization Header\n\nFor convenience, the `@Authorization` attribute can be used to set a static `\"Authorization\"` header.\n\n```swift\n@Authorization(.basic(username: \"joshuawright11\", password: \"P@ssw0rd\"))\nprotocol Users {\n    ...\n}\n```\n\n### Body\n\nFunction parameters on a request that _isn't_ a `@GET`, `@HEAD`, or `@DELETE` are inferred to be a field in the body.\n\n```swift\n@POST(\"/todo\")\nfunc createTodo(name: String, isDone: Bool, tags: [String]) async throws\n```\n\nIf you need to explicitly mark a parameter as a body field, use `Field\u003cT\u003e`.\n\n```swift\n@POST(\"/todo\")\nfunc createTodo(name: Field\u003cString\u003e, isDone: Field\u003cBool\u003e, tags: Field\u003c[String]\u003e) async throws\n```\n\n#### `Body\u003cT\u003e`\n\nAternatively, the entire request body can be set using `Body\u003cT\u003e`. An endpoint can only have one `Body\u003cT\u003e` parameter and it is mutually exclusive with `Field\u003cT\u003e`.\n\n```swift\nstruct Todo: Codable {\n    let name: String\n    let isDone: Bool\n    let tags: [String]\n}\n\n@POST(\"/todo\")\nfunc createTodo(todo: Body\u003cTodo\u003e) async throws\n```\n\n#### Body Encoding\n\nBy default, all `Body` and `Field` parameters are encoded as `application/json`. You can encode with a custom `JSONEncoder` using the `@JSON` attribute.\n\n```swift\nextension JSONEncoder {\n    static var iso8601: JSONEncoder {\n        let encoder = JSONEncoder()\n        encoder.dateEncodingStrategy = .iso8601\n        return encoder\n    }\n}\n\n@JSON(encoder: .iso8601)\n@POST(\"/user\")\nfunc createUser(username: String, password: String) async throws\n```\n\n##### URLForm\n\nYou may encode body parameters as `application/x-www-form-urlencoded` using `@URLForm`.\n\n```swift\n@URLForm\n@POST(\"/todo\")\nfunc createTodo(name: String, isDone: Bool, tags: [String]) async throws\n```\n\n##### Multipart\n\nYou can also encode body parameters as `multipart/form-data` using `@Multipart`. If you do, all body parameters must be of type `Part`.\n\n```swift\n@Multipart\n@POST(\"/attachments\")\nfunc uploadAttachments(file1: Part, file2: Part) async throws\n```\n\n##### Global Encoding\n\nYou can attribute your protocol with an encoding attribute to encode all requests as such.\n\n```swift\n@API\n@URLForm\nprotocol Todos {\n    @POST(\"/todo\")\n    func createTodo(name: String, isDone: Bool, tags: [String]) async throws\n\n    @PATCH(\"/todo/:id\")\n    func updateTodo(id: Int, name: String, isDone: Bool, tags: [String]) async throws\n}\n```\n\n##### Custom Body Encoders\n\nIf you'd like to use a custom encoder, you may pass them as arguments to `@JSON`, `@URLForm` and `@Multipart`.\n\n```swift\nextension JSONEncoder {\n    static var iso8601: JSONEncoder {\n        let encoder = JSONEncoder()\n        encoder.dateEncodingStrategy = .iso8601\n        return encoder\n    }\n}\n\n@JSON(encoder: .iso8601)\nprotocol Todos { ... }\n```\n\n## Responses\n\nThe return type of your function tells Papyrus how to handle the endpoint response.\n\n### `Decodable`\n\nIf your function returns a type conforming to `Decodable`, Papyrus will automatically decode it from the response body using `JSONDecoder`.\n\n```swift\n@GET(\"/user\")\nfunc getUser() async throws -\u003e User\n```\n\n### `Data`\n\nIf you only need a response's raw body bytes, you can just return `Data?` or `Data` from your function.\n\n```swift\n@GET(\"/bytes\")\nfunc getBytes() async throws -\u003e Data?\n\n@GET(\"/image\")\nfunc getImage() async throws -\u003e Data // this will throw an error if `GET /image` returns an empty body\n```\n\n### `Void`\n\nIf you just want to confirm the response was successful and don't need to access the body, you may leave out the return type.\n\n```swift\n@DELETE(\"/logout\")\nfunc logout() async throws\n```\n\n### `Response`\n\nIf you want the raw response data, e.g. to access headers, set the return type to `Response`.\n\n```swift\n@GET(\"/user\")\nfunc getUser() async throws -\u003e Response\n\nlet res = try await users.getUser()\nprint(\"The response had headers \\(res.headers)\")\n```\n\nIf you'd like to automatically decode a type AND access the `Response`, you may return a tuple with both.\n\n```swift\n@GET(\"/user\")\nfunc getUser() async throws -\u003e (User, Response)\n\nlet (user, res) = try await users.getUser()\nprint(\"The response status code was: \\(res.statusCode!)\")\n```\n\n### Error Handling\n\nIf any errors occur while making a request, a `PapyrusError` will be thrown. Use it to access any `Request` and `Response` associated with the error.\n\n```swift\n@GET(\"/user\")\nfunc getUser() async throws -\u003e User\n\ndo {\n    let user = try await users.getUser()\n} catch {\n    if let error = error as? PapyrusError {\n        print(\"Error making request \\(error.request): \\(error.message). Response was: \\(error.response)\")\n    }\n}\n```\n\n## Advanced\n\n### Parameter Labels\n\nIf you use two labels for a function parameter, the second one will be inferred as the relevant key.\n\n```swift\n@GET(\"/posts/:postId\")\nfunc getPost(id postId: Int) async throws -\u003e Post\n```\n\n### Key Mapping\n\nOften, you'll want to encode request fields and decode response fields using something other than camelCase. Instead of setting a custom key for each individual attribute, you can use `@KeyMapping` at the function or protocol level.\n\nNote that this affects `Query`, `Body`, and `Field` parameters on requests as well as decoding content from the `Response`.\n\n```swift\n@API\n@KeyMapping(.snakeCase)\nprotocol Todos {\n    ...\n}\n```\n\n### Access Control\n\nWhen you use `@API` or [`@Mock`](#mocking-with-mock), Papyrus will generate an implementation named `\u003cprotocol\u003eAPI` or `\u003cprotocol\u003eMock` respectively. The [access level](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/accesscontrol/) will match the access level of the protocol.\n\n### Request Modifiers\n\nIf you'd like to manually run custom request build logic before executing any request on a provider, you may use the `modifyRequests()` function.\n\n```swift\nlet provider = Provider(baseURL: \"https://sandbox.plaid.com\")\n    .modifyRequests { (req: inout RequestBuilder) in\n        req.addField(\"client_id\", value: \"\u003cclient-id\u003e\")\n        req.addField(\"secret\", value: \"\u003csecret\u003e\")\n    }\nlet plaid: Plaid = PlaidAPI(provider: provider)\n```\n\n### Interceptors\n\nYou may also inspect a `Provider`'s raw `Request`s and `Response`s using `intercept()`. Make sure to call the second closure parameter if you want the request to continue.\n\n```swift\nlet provider = Provider(baseURL: \"http://localhost:3000\")\n    .intercept { req, next in\n        let start = Date()\n        let res = try await next(req)\n        let elapsedTime = String(format: \"%.2fs\", Date().timeIntervalSince(start))\n        // Got a 200 for GET /users after 0.45s\n        print(\"Got a \\(res.statusCode!) for \\(req.method) \\(req.url!.relativePath) after \\(elapsedTime)\")\n        return res\n    }\n```\n\n### `RequestModifer` \u0026 `Interceptor` protocols\n\nYou can isolate request modifier and interceptor logic to a specific type for use across multiple `Provider`s using the `RequestModifer` and `Interceptor` protocols. Pass them to a `Provider`'s initializer.\n\n```swift\nstruct MyRequestModifier: RequestModifier { ... }\nstruct MyInterceptor: Interceptor { ... }\nlet provider = Provider(baseURL: \"http://localhost:3000\", modifiers: [MyRequestModifier()], interceptors: [MyInterceptor()])\n```\n\n### Callback APIs\n\n[Swift concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html) is the modern way of running asynchronous code in Swift.\n\nIf you haven't yet migrated to Swift concurrency and need access to a callback based API, you can pass an `@escaping` completion handler as the last argument in your endpoint functions.\n\nThe function must have no return type and the closure must have a single argument of type `Result\u003cT: Codable, Error\u003e`, `Result\u003cVoid, Error\u003e`, or `Response` argument.\n\n```swift\n// equivalent to `func getUser() async throws -\u003e User`\n@GET(\"/user\")\nfunc getUser(callback: @escaping (Result\u003cUser, Error\u003e) -\u003e Void)\n\n// equivalent to `func createUser(email: String, password: String) async throws`\n@POST(\"/user\")\nfunc createUser(email: String, password: String, completion: @escaping (Result\u003cVoid, Error\u003e) -\u003e Void)\n\n// equivalent to `func getResponse() async throws -\u003e Response`\n@GET(\"/response\")\nfunc getResponse(completion: @escaping (Response) -\u003e Void)\n```\n\n## Testing\n\nBecause APIs defined with Papyrus are protocols, they're simple to mock in tests; just implement the protocol.\n\nIf you use `Path\u003cT\u003e`, `Header\u003cT\u003e`, `Field\u003cT\u003e`, or `Body\u003cT\u003e` types, you don't need to include them in your protocol conformance. They are just typealiases used to hint Papyrus how to use the parameter.\n\n```swift\n@API\nprotocol GitHub {\n    @GET(\"/users/:username/repos\")\n    func getRepositories(username: String) async throws -\u003e [Repository]\n}\n\nstruct GitHubMock: GitHub {\n    func getRepositories(username: String) async throws -\u003e [Repository] {\n        return [\n            Repository(name: \"papyrus\"),\n            Repository(name: \"alchemy\"),\n            Repository(name: \"fusion\"),\n        ]\n    }\n}\n```\n\nYou can then use your mock during tests when the protocol is required.\n\n```swift\nfunc testCounting() {\n    let mock: GitHub = GitHubMock()\n    let service = MyService(github: mock)\n    let count = service.countRepositories(of: \"joshuawright11\")\n    XCTAssertEqual(count, 3)\n}\n```\n\n### @Mock\n\nFor convenience, you can leverage macros to automatically generated mocks using `@Mock`. Like `@API`, this generates an implementation of your protocol.\n\nThe generated `Mock` type has `mock` functions to easily verify request parameters and mock responses.\n\n```swift\n@API  // Generates `GitHubAPI: GitHub`\n@Mock // Generates `GitHubMock: GitHub`\nprotocol GitHub {\n    @GET(\"/users/:username/repos\")\n    func getRepositories(username: String) async throws -\u003e [Repository]\n}\n\nfunc testCounting() {\n    let mock = GitHubMock()\n    mock.mockGetRepositories { username in\n        XCTAssertEqual(username, \"joshuawright11\")\n        return [\n            Repository(name: \"papyrus\"),\n            Repository(name: \"alchemy\")\n        ]\n    }\n\n    let service = MyService(github: mock)\n    let count = service.countRepositories(of: \"joshuawright11\")\n    XCTAssertEqual(count, 2)\n}\n```\n\n## Contribution\n\n👋 Thanks for checking out Papyrus!\n\nIf you'd like to contribute please [file an issue](https://github.com/joshuawright11/papyrus/issues), [open a pull request](https://github.com/joshuawright11/papyrus/pulls) or [start a discussion](https://github.com/joshuawright11/papyrus/discussions).\n\n## Acknowledgements\n\nPapyrus was heavily inspired by [Retrofit](https://github.com/square/retrofit).\n\n## License\n\nPapyrus is released under an MIT license. See [License.md](License.md) for more information.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshuawright11%2Fpapyrus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoshuawright11%2Fpapyrus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshuawright11%2Fpapyrus/lists"}