Skip to content

GO

Twitter API Go client

The Go client for TwitterAPIs is net/http and encoding/json, both of which are already in your standard library. Authentication is a single static header, so there is no credential machinery to build and no module to add to go.mod. One method on a small struct covers every read endpoint, with typed results and a context you control.

No dependency. net/http and encoding/json are in the standard library.

twitterapis.go
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "net/http"
7 "net/url"
8 "os"
9)
10
11const base = "https://api.twitterapis.com"
12
13func get(path string, params url.Values, out any) error {
14 req, err := http.NewRequest("GET", base+"/twitter/"+path+"?"+params.Encode(), nil)
15 if err != nil {
16 return err
17 }
18 req.Header.Set("Authorization", "Bearer "+os.Getenv("TWITTERAPIS_KEY"))
19
20 res, err := http.DefaultClient.Do(req)
21 if err != nil {
22 return err
23 }
24 defer res.Body.Close()
25 if res.StatusCode != http.StatusOK {
26 return fmt.Errorf("twitterapis: status %d", res.StatusCode)
27 }
28 return json.NewDecoder(res.Body).Decode(out)
29}
30
31type profile struct {
32 User struct {
33 ID string `json:"id"`
34 FollowersCount int `json:"followers_count"`
35 } `json:"user"`
36}
37
38func main() {
39 var p profile
40 if err := get("user/info", url.Values{"username": {"naval"}}, &p); err != nil {
41 panic(err)
42 }
43 fmt.Println(p.User.ID, p.User.FollowersCount)
44}

A reusable client type

Wrapping the key and an http.Client in a struct gives you one place to set the timeout, one place to inject a test transport, and a context-aware method that every call site can cancel.

client.go
1type Client struct {
2 Key string
3 HTTP *http.Client
4}
5
6func New(key string) *Client {
7 return &Client{Key: key, HTTP: &http.Client{Timeout: 30 * time.Second}}
8}
9
10func (c *Client) Get(ctx context.Context, path string, params url.Values, out any) error {
11 u := "https://api.twitterapis.com/twitter/" + path + "?" + params.Encode()
12
13 req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
14 if err != nil {
15 return err
16 }
17 req.Header.Set("Authorization", "Bearer "+c.Key)
18
19 res, err := c.HTTP.Do(req)
20 if err != nil {
21 return err
22 }
23 defer res.Body.Close()
24
25 if res.StatusCode != http.StatusOK {
26 body, _ := io.ReadAll(io.LimitReader(res.Body, 2048))
27 return fmt.Errorf("twitterapis: status %d: %s", res.StatusCode, body)
28 }
29 return json.NewDecoder(res.Body).Decode(out)
30}

Walking a cursor-paginated endpoint

Go has no generators, so the idiomatic shape is a callback the caller can abort by returning an error. That keeps the cursor logic in one function while leaving the decision to stop with the code that is paying for each page.

followers.go
1type followersPage struct {
2 Followers []User `json:"followers"`
3 NextCursor string `json:"next_cursor"`
4}
5
6// Followers walks every page and calls fn once per page. Each page is one
7// billed call, so the caller controls the loop by returning an error to stop.
8func (c *Client) Followers(ctx context.Context, username string, fn func(followersPage) error) error {
9 params := url.Values{"username": {username}}
10 for {
11 var page followersPage
12 if err := c.Get(ctx, "user/followers", params, &page); err != nil {
13 return err
14 }
15 if err := fn(page); err != nil {
16 return err
17 }
18 if page.NextCursor == "" {
19 return nil
20 }
21 params.Set("cursor", page.NextCursor)
22 }
23}

Where to go next

The same client shape in Python and Node, or all six languages on the language clients page. The REST API reference covers the paging model and the status codes this client branches on, and the rate limits page covers what concurrency you can actually run.

Frequently Asked Questions

There is no package to install here, and the client above shows why one would add little. net/http handles the request, encoding/json handles the response, and the only credential work is setting a static header. That is the entire surface an SDK would wrap, so writing it yourself gives you a client you can read in one screen and change without waiting on an upstream release.

Define structs for the fields you use. Decoding into map[string]any works and is fine for exploration, but it pushes every field name into string literals scattered through your code, and a typo becomes a nil at runtime rather than a compile error. The public OpenAPI 3.1 specification will generate the structs for you if you would rather not write them.

Yes. There is no platform-level rate ceiling to design around, so a worker pool over a channel of usernames works without a token bucket in front of it. Keep the pool bounded anyway, because unbounded concurrency turns a transient upstream problem into a large number of simultaneous failures and a matching number of billed calls.

Set a Timeout on the http.Client for a hard ceiling, and use http.NewRequestWithContext so a caller can cancel the request through its own context. Both matter: the client timeout stops a hung connection, and the context lets a request die when the work that needed it has already been abandoned.

Read next_cursor from each response and set it as the cursor parameter on the next request, stopping when it comes back empty. The helper on this page inverts control with a callback so the caller can stop early, which matters when each page is a separate billed call and a large account has a lot of them.

$0.0008 for a standard read, the same everywhere, billed per call rather than per plan. The client makes exactly one request per Get, so your bill is the number of times your code calls it. Retries and pagination loops are where a count grows without anybody deciding it should.