Skip to content

Commit e942f18

Browse files
[Feat] Refactor: Centralize API Request Logic with Helper Methods (#97)
* Refactor: Centralize API Request Logic with Helper Methods This commit refactors the Postmark API client to centralize and simplify how API requests are made. The previous implementation involved repetitive boilerplate code for constructing requests, handling authentication, and building URLs with query parameters. The key changes include: - A new `helpers.go` file with `buildURL` and `buildURLWithQuery` functions to eliminate duplicate URL construction logic. - New helper methods on the `Client` struct (`get`, `post`, `put`, `patch`, `delete`) that wrap the underlying `doRequest` function. This provides a cleaner, more expressive, and consistent interface for making API calls. Variants for both server and account tokens have been included. - The `doRequest` function signature has been simplified, removing the need for the `parameters` struct. - All resource files (`email.go`, `templates.go`, `bounce.go`, etc.) and test files have been updated to use these new helper methods, significantly reducing code duplication and improving readability. - Corrected error handling for `DELETE`, `PUT`, and `POST` requests that may return an `ErrorCode` in the body even with a successful HTTP status. * docs: fix alignment in Key Files table in CLAUDE.md Adjust the markdown table in the CLAUDE.md file to improve column alignment and readability. This change ensures consistent spacing and a cleaner presentation of file purposes in the documentation. * fix(postmark): improve error wrapping and simplify query check Use error wrapping (%w) when unmarshalling API errors to preserve original error context, aiding in better error handling and debugging. Simplify buildURLWithQuery function by removing redundant nil check for query parameters, improving code clarity without changing behavior. * chore: remove unused email-related source files Remove helpers.go, postmark.go, email.go, sender_signatures.go, inbound_rules_triggers.go, and data_removals.go as they are no longer needed. This cleanup reduces code clutter and improves maintainability. * test: add HTTP server benchmarks for bounce and data removal APIs Add comprehensive benchmarks for Bounce and Data Removal client methods using httptest servers. The benchmarks simulate realistic HTTP responses for endpoints like GetDeliveryStats, GetBounces, GetBounce, GetBounceDump, ActivateBounce, GetBouncedTags, CreateDataRemoval, and GetDataRemovalStatus. This change improves benchmark accuracy by exercising the full HTTP client logic and request handling rather than isolated code, enabling more reliable performance assessments and future optimizations. * test: enhance benchmarks with HTTP test server mocks Refactor benchmarks in inbound_rules_triggers_test.go and domains_test.go to use httptest.NewServer and test routers for mocking API responses. Add HTTP handlers returning predefined JSON responses for all benchmarked endpoints, enabling more realistic simulations of client-server interaction. This improves benchmark accuracy by verifying request handling and response processing, replacing previous no-op variable usage and manual context setup. * test: add HTTP mock servers to benchmarks for stable results Refactor benchmarks to use httptest servers and mock HTTP handlers for all client calls in stats, messages_outbound, and sender_signatures packages. This ensures benchmarks run against controlled, consistent responses rather than live or nil backends. The change improves benchmark reliability, removes dependencies on external services, and enables more accurate performance measurement of client methods by simulating realistic API responses. * docs: add detailed API client performance results to README Include comprehensive benchmark results measuring real API client performance on Apple M1 Max, covering latency, throughput, memory usage, and allocations for all major API operations. Add expandable sections with detailed per-operation metrics to provide transparency and help users understand the efficiency and speed of the client. --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent 6a31d3a commit e942f18

30 files changed

+1419
-897
lines changed

.github/CLAUDE.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,15 @@ magex help # View all commands
118118

119119
## 📁 Key Files
120120

121-
| File | Purpose |
122-
|------|---------|
123-
| `postmark.go` | Core client, doRequest method, auth headers |
124-
| `email.go` | Email sending, batch operations |
125-
| `templates.go` | Template CRUD, templated email sending |
126-
| `data_removals.go` | GDPR data removal API |
127-
| `webhooks.go` | Webhook management |
128-
| `test_router.go` | Custom HTTP router for testing |
129-
| `examples/examples.go` | Usage examples for all major features |
121+
| File | Purpose |
122+
|------------------------|---------------------------------------------|
123+
| `postmark.go` | Core client, doRequest method, auth headers |
124+
| `email.go` | Email sending, batch operations |
125+
| `templates.go` | Template CRUD, templated email sending |
126+
| `data_removals.go` | GDPR data removal API |
127+
| `webhooks.go` | Webhook management |
128+
| `test_router.go` | Custom HTTP router for testing |
129+
| `examples/examples.go` | Usage examples for all major features |
130130

131131
## ⚠️ Important Notes
132132

README.md

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,151 @@ Run the Go benchmarks:
407407
magex bench
408408
```
409409

410+
### 📊 Performance Results
411+
412+
All benchmarks measure **real API client performance** including HTTP request setup, JSON marshaling/unmarshalling, and response processing against mock servers. Results collected on Apple M1 Max (10 cores).
413+
414+
#### 🎯 Performance Overview
415+
416+
| Metric | Value | Description |
417+
|-----------------------|-----------------|--------------------------|
418+
| **Fastest Operation** | 36.7 µs | Get Bounced Tags |
419+
| **Average Latency** | 41.2 µs | Across all 47 operations |
420+
| **Throughput** | ~24,000 ops/sec | Per operation average |
421+
| **Memory Efficiency** | 7.7 KB/op | Average memory usage |
422+
| **Allocations** | 97 allocs/op | Average per operation |
423+
424+
<details>
425+
<summary><strong>Bounce API Performance</strong></summary>
426+
<br/>
427+
428+
| Operation | Latency (µs) | Throughput (ops/sec) | Memory | Allocs |
429+
|--------------------|--------------|----------------------|--------|--------|
430+
| Get Delivery Stats | 38.0 | 26,300 | 6.8 KB | 86 |
431+
| Get Bounces | 41.6 | 24,000 | 7.8 KB | 110 |
432+
| Get Bounce | 40.4 | 24,800 | 7.1 KB | 89 |
433+
| Get Bounce Dump | 37.4 | 26,700 | 6.6 KB | 84 |
434+
| Activate Bounce | 39.9 | 25,100 | 7.2 KB | 92 |
435+
| Get Bounced Tags | 36.7 | 27,200 | 6.6 KB | 85 |
436+
437+
</details>
438+
439+
<details>
440+
<summary><strong>Data Removal API Performance</strong></summary>
441+
<br/>
442+
443+
| Operation | Latency (µs) | Throughput (ops/sec) | Memory | Allocs |
444+
|-------------------------|--------------|----------------------|--------|--------|
445+
| Create Data Removal | 41.5 | 24,100 | 7.6 KB | 100 |
446+
| Get Data Removal Status | 38.6 | 25,900 | 6.8 KB | 86 |
447+
448+
</details>
449+
450+
<details>
451+
<summary><strong>Domains API Performance</strong></summary>
452+
<br/>
453+
454+
| Operation | Latency (µs) | Throughput (ops/sec) | Memory | Allocs |
455+
|--------------------|--------------|----------------------|--------|--------|
456+
| Get Domains | 40.2 | 24,900 | 7.2 KB | 101 |
457+
| Get Domain | 41.9 | 23,900 | 7.3 KB | 89 |
458+
| Create Domain | 41.3 | 24,200 | 7.8 KB | 100 |
459+
| Edit Domain | 41.7 | 24,000 | 8.3 KB | 107 |
460+
| Delete Domain | 38.2 | 26,200 | 7.1 KB | 89 |
461+
| Verify DKIM Status | 39.6 | 25,200 | 7.4 KB | 91 |
462+
| Verify Return Path | 39.2 | 25,500 | 7.4 KB | 90 |
463+
| Rotate DKIM | 40.2 | 24,900 | 7.6 KB | 93 |
464+
465+
</details>
466+
467+
<details>
468+
<summary><strong>Inbound Rules Triggers API Performance</strong></summary>
469+
<br/>
470+
471+
| Operation | Latency (µs) | Throughput (ops/sec) | Memory | Allocs |
472+
|-----------------------------|--------------|----------------------|--------|--------|
473+
| Get Inbound Rule Triggers | 39.8 | 25,100 | 7.1 KB | 101 |
474+
| Create Inbound Rule Trigger | 41.0 | 24,400 | 7.6 KB | 99 |
475+
| Delete Inbound Rule Trigger | 40.4 | 24,700 | 6.7 KB | 84 |
476+
477+
</details>
478+
479+
<details>
480+
<summary><strong>Message Streams API Performance</strong></summary>
481+
<br/>
482+
483+
| Operation | Latency (µs) | Throughput (ops/sec) | Memory | Allocs |
484+
|--------------------------|--------------|----------------------|--------|--------|
485+
| List Message Streams | 44.4 | 22,500 | 7.4 KB | 93 |
486+
| Get Message Stream | 42.6 | 23,500 | 7.0 KB | 89 |
487+
| Edit Message Stream | 46.8 | 21,400 | 8.1 KB | 106 |
488+
| Create Message Stream | 44.5 | 22,500 | 8.1 KB | 104 |
489+
| Archive Message Stream | 40.4 | 24,800 | 6.8 KB | 86 |
490+
| Unarchive Message Stream | 42.6 | 23,500 | 7.1 KB | 90 |
491+
492+
</details>
493+
494+
<details>
495+
<summary><strong>Messages API Performance</strong></summary>
496+
<br/>
497+
498+
| Operation | Latency (µs) | Throughput (ops/sec) | Memory | Allocs |
499+
|------------------------------|--------------|----------------------|--------|--------|
500+
| Get Outbound Messages Clicks | 47.5 | 21,100 | 8.5 KB | 118 |
501+
| Get Outbound Message Clicks | 43.2 | 23,100 | 7.9 KB | 109 |
502+
503+
</details>
504+
505+
<details>
506+
<summary><strong>Sender Signatures API Performance</strong></summary>
507+
<br/>
508+
509+
| Operation | Latency (µs) | Throughput (ops/sec) | Memory | Allocs |
510+
|-------------------------------|--------------|----------------------|--------|--------|
511+
| Get Sender Signatures | 40.6 | 24,600 | 7.3 KB | 104 |
512+
| Get Sender Signature | 40.6 | 24,600 | 7.5 KB | 92 |
513+
| Create Sender Signature | 42.2 | 23,700 | 8.1 KB | 101 |
514+
| Edit Sender Signature | 47.1 | 21,200 | 8.6 KB | 108 |
515+
| Delete Sender Signature | 38.8 | 25,800 | 7.1 KB | 89 |
516+
| Resend Signature Confirmation | 39.0 | 25,700 | 7.2 KB | 90 |
517+
518+
</details>
519+
520+
<details>
521+
<summary><strong>Stats API Performance</strong></summary>
522+
<br/>
523+
524+
| Operation | Latency (µs) | Throughput (ops/sec) | Memory | Allocs |
525+
|---------------------------|--------------|----------------------|--------|--------|
526+
| Get Click Counts | 40.3 | 24,800 | 7.4 KB | 103 |
527+
| Get Browser Family Counts | 42.2 | 23,700 | 7.6 KB | 103 |
528+
| Get Click Location Counts | 42.8 | 23,400 | 7.4 KB | 103 |
529+
| Get Click Platform Counts | 42.0 | 23,800 | 7.5 KB | 103 |
530+
| Get Email Client Counts | 41.3 | 24,200 | 7.6 KB | 103 |
531+
532+
</details>
533+
534+
<details>
535+
<summary><strong>Templates API Performance</strong></summary>
536+
<br/>
537+
538+
| Operation | Latency (µs) | Throughput (ops/sec) | Memory | Allocs |
539+
|----------------------------|--------------|----------------------|--------|--------|
540+
| Get Template | 39.9 | 25,100 | 7.5 KB | 92 |
541+
| Get Templates | 41.3 | 24,200 | 7.5 KB | 103 |
542+
| Get Templates Filtered | 40.0 | 25,000 | 7.4 KB | 103 |
543+
| Create Template | 44.7 | 22,400 | 7.9 KB | 99 |
544+
| Edit Template | 42.5 | 23,500 | 8.4 KB | 106 |
545+
| Delete Template | 39.3 | 25,500 | 7.1 KB | 89 |
546+
| Validate Template | 44.9 | 22,300 | 8.5 KB | 110 |
547+
| Send Templated Email | 44.2 | 22,600 | 8.8 KB | 110 |
548+
| Send Templated Email Batch | 46.1 | 21,700 | 9.0 KB | 117 |
549+
| Push Templates | 42.9 | 23,300 | 7.9 KB | 105 |
550+
551+
</details>
552+
553+
> **Note:** All benchmarks use mock HTTP servers for consistent, reproducible measurements. Real-world performance will vary based on network latency and Postmark API response times.
554+
410555
<br/>
411556

412557
## 🛠️ Code Standards

bounce.go

Lines changed: 12 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ package postmark
22

33
import (
44
"context"
5-
"encoding/json"
65
"fmt"
7-
"net/http"
8-
"net/url"
96
"time"
107
)
118

@@ -31,12 +28,7 @@ type DeliveryStats struct {
3128
// GetDeliveryStats returns delivery stats for the server
3229
func (client *Client) GetDeliveryStats(ctx context.Context) (DeliveryStats, error) {
3330
res := DeliveryStats{}
34-
path := "deliverystats"
35-
err := client.doRequest(ctx, parameters{
36-
Method: http.MethodGet,
37-
Path: path,
38-
TokenType: serverToken,
39-
}, &res)
31+
err := client.get(ctx, "deliverystats", &res)
4032
return res, err
4133
}
4234

@@ -89,33 +81,21 @@ type bouncesResponse struct {
8981
func (client *Client) GetBounces(ctx context.Context, count, offset int64, options map[string]interface{}) ([]Bounce, int64, error) {
9082
res := bouncesResponse{}
9183

92-
values := &url.Values{}
93-
values.Add("count", fmt.Sprintf("%d", count))
94-
values.Add("offset", fmt.Sprintf("%d", offset))
95-
96-
for k, v := range options {
97-
values.Add(k, fmt.Sprintf("%v", v))
84+
if options == nil {
85+
options = make(map[string]interface{})
9886
}
9987

100-
path := fmt.Sprintf("bounces?%s", values.Encode())
88+
options["count"] = count
89+
options["offset"] = offset
10190

102-
err := client.doRequest(ctx, parameters{
103-
Method: http.MethodGet,
104-
Path: path,
105-
TokenType: serverToken,
106-
}, &res)
91+
err := client.get(ctx, buildURL("bounces", options), &res)
10792
return res.Bounces, res.TotalCount, err
10893
}
10994

11095
// GetBounce fetches a single bounce with bounceID
11196
func (client *Client) GetBounce(ctx context.Context, bounceID int64) (Bounce, error) {
11297
res := Bounce{}
113-
path := fmt.Sprintf("bounces/%v", bounceID)
114-
err := client.doRequest(ctx, parameters{
115-
Method: http.MethodGet,
116-
Path: path,
117-
TokenType: serverToken,
118-
}, &res)
98+
err := client.get(ctx, fmt.Sprintf("bounces/%v", bounceID), &res)
11999
return res, err
120100
}
121101

@@ -126,12 +106,7 @@ type dumpResponse struct {
126106
// GetBounceDump fetches an SMTP data dump for a single bounce
127107
func (client *Client) GetBounceDump(ctx context.Context, bounceID int64) (string, error) {
128108
res := dumpResponse{}
129-
path := fmt.Sprintf("bounces/%v/dump", bounceID)
130-
err := client.doRequest(ctx, parameters{
131-
Method: http.MethodGet,
132-
Path: path,
133-
TokenType: serverToken,
134-
}, &res)
109+
err := client.get(ctx, fmt.Sprintf("bounces/%v/dump", bounceID), &res)
135110
return res.Body, err
136111
}
137112

@@ -145,37 +120,13 @@ type activateBounceResponse struct {
145120
// TODO: clarify this with Postmark
146121
func (client *Client) ActivateBounce(ctx context.Context, bounceID int64) (Bounce, string, error) {
147122
res := activateBounceResponse{}
148-
path := fmt.Sprintf("bounces/%v/activate", bounceID)
149-
err := client.doRequest(ctx, parameters{
150-
Method: http.MethodPut,
151-
Path: path,
152-
TokenType: serverToken,
153-
}, &res)
123+
err := client.put(ctx, fmt.Sprintf("bounces/%v/activate", bounceID), nil, &res)
154124
return res.Bounce, res.Message, err
155125
}
156126

157-
type bouncedTagsResponse struct {
158-
Tags []string `json:"tags"`
159-
}
160-
161127
// GetBouncedTags retrieves a list of tags that have generated bounced emails
162128
func (client *Client) GetBouncedTags(ctx context.Context) ([]string, error) {
163-
var raw json.RawMessage
164-
path := "bounces/tags"
165-
err := client.doRequest(ctx, parameters{
166-
Method: http.MethodGet,
167-
Path: path,
168-
TokenType: serverToken,
169-
}, &raw)
170-
if err != nil {
171-
return []string{}, err
172-
}
173-
174-
// PM returns this payload in an impossible to unmarshal way
175-
// ["tag1","tag2","tag3"]. So let's rejigger it to make it possible.
176-
jsonString := fmt.Sprintf(`{"tags": %s}`, string(raw))
177-
res := bouncedTagsResponse{}
178-
err = json.Unmarshal([]byte(jsonString), &res)
179-
180-
return res.Tags, err
129+
var res []string
130+
err := client.get(ctx, "bounces/tags", &res)
131+
return res, err
181132
}

0 commit comments

Comments
 (0)