Skip to content

Commit 028e774

Browse files
authored
feat: full Figma REST API coverage (46/46 endpoints) (#1)
* feat: expand Figma REST API coverage to 46/46 endpoints (100%) Add 38 new endpoints covering the complete Figma REST API v0.36.0: - User: GET /me - Files: HEAD /files/:key, GET /files/:key/images, GET /files/:key/versions, GET /files/:key/variables/published - Comments: GET/POST/DELETE /files/:key/comments - Components: GET /components/:key, GET /teams/:id/components - Component Sets: GET /component_sets/:key, GET /files/:key/component_sets, GET /teams/:id/component_sets - Styles: GET /styles/:key, GET /teams/:id/styles - Projects: GET /teams/:id/projects, GET /projects/:id/files - Reactions: GET/POST/DELETE /files/:key/comments/:id/reactions - Dev Resources: GET/POST/PUT/DELETE /files/:key/dev_resources - Webhooks (v2): GET/POST/PUT/DELETE /v2/webhooks, GET /v2/teams/:id/webhooks, GET /v2/webhooks/:id/requests - Analytics: activity_logs, payments, library component/style/variable actions and usages Refactor baseURL from https://api.figma.com/v1/ to https://api.figma.com/ so endpoints can target both v1 and v2 paths. This is an internal change with no impact on the public API. Add PaginationParams for cursor-based team endpoints, EmptyResponse for DELETE operations, and 22 new JSON fixtures with 290 total test cases. * fix: align endpoints with Figma REST API spec and fix critical bugs - Fix DELETE endpoints crashing on empty 204 No Content responses by adding explicit content(from:with:) that handles empty body - Fix POST/PUT dev_resources to use correct path /v1/dev_resources with batch request/response wrapping (dev_resources/links_created/links_updated) - Fix PostDevResourceBody to include required file_key field per spec - Fix PutDevResourceBody to match spec (remove dev_status, keep id/name/url) - Fix GetPaymentsEndpoint to use query params per spec instead of path params - Fix GetWebhooksEndpoint to use query params (context, context_id) per spec - Fix GetActivityLogsEndpoint to use correct query filters per spec - Fix GetFileMetaEndpointTests for Linux FoundationNetworking compatibility - Remove incorrect @available deprecation from GetTeamWebhooksEndpoint - Add pageSize validation precondition to PaginationParams - Narrow Codable to Decodable for read-only models (Webhook, DevResource, FileVersion, ComponentSet, User) * fix: expose FigmaClient.baseURL as public constant Prevents downstream clients (e.g. ExFig) from hardcoding the base URL in their mock clients, which would silently break when the URL format changes (as it did when moving version prefix into individual endpoints).
1 parent 6fc9071 commit 028e774

140 files changed

Lines changed: 4260 additions & 33 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ A Swift client for the [Figma REST API](https://www.figma.com/developers/api) wi
99

1010
## Features
1111

12-
- 8 Figma API endpoints (files, components, nodes, images, styles, variables)
12+
- Full Figma REST API coverage (46 endpoints) — files, components, styles, variables, comments, webhooks, dev resources, analytics, and more
1313
- Token-bucket rate limiting with fair round-robin scheduling
1414
- Exponential backoff retry with jitter and `Retry-After` support
15-
- Figma Variables API (read + write codeSyntax)
15+
- Figma Variables API (read local, read published, write codeSyntax)
16+
- Support for both API v1 and v2 (webhooks)
1617
- GitHub Releases endpoint (for version checking)
1718
- Swift 6 strict concurrency
1819

@@ -47,12 +48,18 @@ Then add `FigmaAPI` to your target dependencies:
4748
```swift
4849
import FigmaAPI
4950

50-
// Create a client with your Figma personal access token
51+
// Create a client
52+
let figma = FigmaClient(accessToken: "your-figma-token", timeout: nil)
53+
54+
// Wrap with rate limiting and retry
55+
let rateLimiter = SharedRateLimiter()
5156
let client = RateLimitedClient(
52-
inner: FigmaClient(accessToken: "your-figma-token")
57+
client: figma,
58+
rateLimiter: rateLimiter,
59+
configID: "default"
5360
)
5461

55-
// Fetch components
62+
// Fetch components from a file
5663
let components = try await client.request(
5764
ComponentsEndpoint(fileId: "your-file-id")
5865
)
@@ -64,8 +71,22 @@ let variables = try await client.request(
6471

6572
// Export images as SVG
6673
let images = try await client.request(
67-
ImageEndpoint(fileId: "your-file-id", nodeIds: ["1:2", "3:4"], format: .svg)
74+
ImageEndpoint(fileId: "your-file-id", nodeIds: ["1:2", "3:4"], params: SVGParams())
6875
)
76+
77+
// Get current user
78+
let me = try await client.request(GetMeEndpoint())
79+
80+
// Post a comment
81+
let comment = try await client.request(
82+
PostCommentEndpoint(
83+
fileId: "your-file-id",
84+
body: PostCommentBody(message: "Looks great!")
85+
)
86+
)
87+
88+
// List webhooks (v2 API)
89+
let webhooks = try await client.request(GetWebhooksEndpoint())
6990
```
7091

7192
## License

Sources/FigmaAPI/Endpoint/BaseEndpoint.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ extension BaseEndpoint {
3737
}
3838
}
3939
}
40+

Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public struct ComponentsEndpoint: BaseEndpoint {
1818

1919
public func makeRequest(baseURL: URL) -> URLRequest {
2020
let url = baseURL
21+
.appendingPathComponent("v1")
2122
.appendingPathComponent("files")
2223
.appendingPathComponent(fileId)
2324
.appendingPathComponent("components")
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Foundation
2+
import YYJSON
3+
#if canImport(FoundationNetworking)
4+
import FoundationNetworking
5+
#endif
6+
7+
public struct DeleteCommentEndpoint: BaseEndpoint {
8+
public typealias Content = EmptyResponse
9+
10+
private let fileId: String
11+
private let commentId: String
12+
13+
public init(fileId: String, commentId: String) {
14+
self.fileId = fileId
15+
self.commentId = commentId
16+
}
17+
18+
public func makeRequest(baseURL: URL) -> URLRequest {
19+
let url = baseURL
20+
.appendingPathComponent("v1")
21+
.appendingPathComponent("files")
22+
.appendingPathComponent(fileId)
23+
.appendingPathComponent("comments")
24+
.appendingPathComponent(commentId)
25+
var request = URLRequest(url: url)
26+
request.httpMethod = "DELETE"
27+
return request
28+
}
29+
30+
public func content(from _: URLResponse?, with body: Data) throws -> EmptyResponse {
31+
if body.isEmpty { return EmptyResponse() }
32+
return try YYJSONDecoder().decode(EmptyResponse.self, from: body)
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Foundation
2+
import YYJSON
3+
#if canImport(FoundationNetworking)
4+
import FoundationNetworking
5+
#endif
6+
7+
public struct DeleteDevResourceEndpoint: BaseEndpoint {
8+
public typealias Content = EmptyResponse
9+
10+
private let fileId: String
11+
private let resourceId: String
12+
13+
public init(fileId: String, resourceId: String) {
14+
self.fileId = fileId
15+
self.resourceId = resourceId
16+
}
17+
18+
public func makeRequest(baseURL: URL) -> URLRequest {
19+
let url = baseURL
20+
.appendingPathComponent("v1")
21+
.appendingPathComponent("files")
22+
.appendingPathComponent(fileId)
23+
.appendingPathComponent("dev_resources")
24+
.appendingPathComponent(resourceId)
25+
var request = URLRequest(url: url)
26+
request.httpMethod = "DELETE"
27+
return request
28+
}
29+
30+
public func content(from _: URLResponse?, with body: Data) throws -> EmptyResponse {
31+
if body.isEmpty { return EmptyResponse() }
32+
return try YYJSONDecoder().decode(EmptyResponse.self, from: body)
33+
}
34+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import Foundation
2+
import YYJSON
3+
#if canImport(FoundationNetworking)
4+
import FoundationNetworking
5+
#endif
6+
7+
public struct DeleteReactionEndpoint: BaseEndpoint {
8+
public typealias Content = EmptyResponse
9+
10+
private let fileId: String
11+
private let commentId: String
12+
private let emoji: String
13+
14+
public init(fileId: String, commentId: String, emoji: String) {
15+
self.fileId = fileId
16+
self.commentId = commentId
17+
self.emoji = emoji
18+
}
19+
20+
public func makeRequest(baseURL: URL) throws -> URLRequest {
21+
let url = baseURL
22+
.appendingPathComponent("v1")
23+
.appendingPathComponent("files")
24+
.appendingPathComponent(fileId)
25+
.appendingPathComponent("comments")
26+
.appendingPathComponent(commentId)
27+
.appendingPathComponent("reactions")
28+
guard var comps = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
29+
throw URLError(.badURL)
30+
}
31+
comps.queryItems = [URLQueryItem(name: "emoji", value: emoji)]
32+
guard let finalURL = comps.url else {
33+
throw URLError(.badURL)
34+
}
35+
var request = URLRequest(url: finalURL)
36+
request.httpMethod = "DELETE"
37+
return request
38+
}
39+
40+
public func content(from _: URLResponse?, with body: Data) throws -> EmptyResponse {
41+
if body.isEmpty { return EmptyResponse() }
42+
return try YYJSONDecoder().decode(EmptyResponse.self, from: body)
43+
}
44+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Foundation
2+
import YYJSON
3+
#if canImport(FoundationNetworking)
4+
import FoundationNetworking
5+
#endif
6+
7+
public struct DeleteWebhookEndpoint: BaseEndpoint {
8+
public typealias Content = EmptyResponse
9+
10+
private let webhookId: String
11+
12+
public init(webhookId: String) {
13+
self.webhookId = webhookId
14+
}
15+
16+
public func makeRequest(baseURL: URL) -> URLRequest {
17+
let url = baseURL
18+
.appendingPathComponent("v2")
19+
.appendingPathComponent("webhooks")
20+
.appendingPathComponent(webhookId)
21+
var request = URLRequest(url: url)
22+
request.httpMethod = "DELETE"
23+
return request
24+
}
25+
26+
public func content(from _: URLResponse?, with body: Data) throws -> EmptyResponse {
27+
if body.isEmpty { return EmptyResponse() }
28+
return try YYJSONDecoder().decode(EmptyResponse.self, from: body)
29+
}
30+
}

Sources/FigmaAPI/Endpoint/FileMetadataEndpoint.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public struct FileMetadataEndpoint: BaseEndpoint {
1616

1717
public func makeRequest(baseURL: URL) throws -> URLRequest {
1818
let url = baseURL
19+
.appendingPathComponent("v1")
1920
.appendingPathComponent("files")
2021
.appendingPathComponent(fileId)
2122

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Foundation
2+
#if canImport(FoundationNetworking)
3+
import FoundationNetworking
4+
#endif
5+
6+
public struct GetActivityLogsEndpoint: BaseEndpoint {
7+
public typealias Content = [ActivityLog]
8+
9+
private let events: String?
10+
private let startTime: Double?
11+
private let endTime: Double?
12+
private let limit: Int?
13+
private let order: String?
14+
15+
public init(
16+
events: String? = nil,
17+
startTime: Double? = nil,
18+
endTime: Double? = nil,
19+
limit: Int? = nil,
20+
order: String? = nil
21+
) {
22+
self.events = events
23+
self.startTime = startTime
24+
self.endTime = endTime
25+
self.limit = limit
26+
self.order = order
27+
}
28+
29+
func content(from root: ActivityLogsResponse) -> [ActivityLog] {
30+
root.activityLogs
31+
}
32+
33+
public func makeRequest(baseURL: URL) throws -> URLRequest {
34+
let url = baseURL
35+
.appendingPathComponent("v1")
36+
.appendingPathComponent("activity_logs")
37+
guard var comps = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
38+
throw URLError(.badURL)
39+
}
40+
var items: [URLQueryItem] = []
41+
if let events { items.append(URLQueryItem(name: "events", value: events)) }
42+
if let startTime { items.append(URLQueryItem(name: "start_time", value: "\(startTime)")) }
43+
if let endTime { items.append(URLQueryItem(name: "end_time", value: "\(endTime)")) }
44+
if let limit { items.append(URLQueryItem(name: "limit", value: "\(limit)")) }
45+
if let order { items.append(URLQueryItem(name: "order", value: order)) }
46+
if !items.isEmpty { comps.queryItems = items }
47+
guard let finalURL = comps.url else {
48+
throw URLError(.badURL)
49+
}
50+
return URLRequest(url: finalURL)
51+
}
52+
}
53+
54+
struct ActivityLogsResponse: Decodable {
55+
let activityLogs: [ActivityLog]
56+
57+
private enum CodingKeys: String, CodingKey {
58+
case activityLogs = "activity_logs"
59+
}
60+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Foundation
2+
#if canImport(FoundationNetworking)
3+
import FoundationNetworking
4+
#endif
5+
6+
public struct GetCommentsEndpoint: BaseEndpoint {
7+
public typealias Content = [Comment]
8+
9+
private let fileId: String
10+
11+
public init(fileId: String) {
12+
self.fileId = fileId
13+
}
14+
15+
func content(from root: CommentsResponse) -> [Comment] {
16+
root.comments
17+
}
18+
19+
public func makeRequest(baseURL: URL) -> URLRequest {
20+
let url = baseURL
21+
.appendingPathComponent("v1")
22+
.appendingPathComponent("files")
23+
.appendingPathComponent(fileId)
24+
.appendingPathComponent("comments")
25+
return URLRequest(url: url)
26+
}
27+
}
28+
29+
struct CommentsResponse: Decodable {
30+
let comments: [Comment]
31+
}

0 commit comments

Comments
 (0)