Skip to content

Repository files navigation

SerpApi Zig Library

serpapi-zig

Warning

This library is under heavy development and is not production ready. The API may change without notice between releases.

Integrate search data into your AI workflow, RAG / fine-tuning, or Zig application using this wrapper for SerpApi.

SerpApi supports Google, Google Maps, Google Shopping, Baidu, Yandex, Yahoo, eBay, App Stores, and more.

Query a vast range of data at scale, including web search results, flight schedules, stock market data, news headlines, and more.

Features

  • persistent → Keep socket connection open to save on SSL handshake / reconnection (2x faster).
  • zero dependency → only the Zig standard library (std.http, std.json), nothing else to fetch.
  • cross platform → Linux, macOS, and Windows.
  • extensive documentation → easy to follow.

Installation

Zig 0.16.0 or higher is required.

Add the dependency to your project:

zig fetch --save 'git+https://github.com/serpapi/serpapi-zig#v1.0.0'

Then wire the module in your build.zig:

const serpapi = b.dependency("serpapi", .{
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("serpapi", serpapi.module("serpapi"));

Versioning

Zig has no central package registry, so there is no version range to resolve: a dependency is a URL plus a content hash. Releases are published as git tags named vMAJOR.MINOR.PATCH, following semantic versioning, and the fragment after # selects which one you get:

# a released version (recommended)
zig fetch --save 'git+https://github.com/serpapi/serpapi-zig#v1.0.0'

# the development branch — moves, may break
zig fetch --save 'git+https://github.com/serpapi/serpapi-zig'

# an exact commit; the SHA must be the full 40 characters
zig fetch --save 'git+https://github.com/serpapi/serpapi-zig#4451c6c4b5a2dc68d7249a2a97f032470ab2070d'

Whichever form you use, --save resolves it at fetch time and records the result as an immutable pin, so a tag that is later moved cannot change your build:

.dependencies = .{
    .serpapi = .{
        .url = "git+https://github.com/serpapi/serpapi-zig?ref=v1.0.0#b7b6dbd303fb36b3331fc8265bc28925ba4ca2a5",
        .hash = "serpapi-1.0.0-PYmxE0B-AABZru95tORJSVF468kIbL_5Ri3-JC6bZBxT",
    },
},

The hash — not the URL — is what actually identifies the package, so always upgrade by re-running zig fetch --save with the new tag, which rewrites the URL, the resolved commit and the hash together. Editing the version in the URL by hand does not upgrade anything: the old hash still resolves to the old package, and the build quietly keeps using it.

A release tarball works too, and produces the same hash as its tag:

zig fetch --save 'https://github.com/serpapi/serpapi-zig/archive/refs/tags/v1.0.0.tar.gz'

Released versions are listed on the releases page; changes are recorded in CHANGELOG.md.

Simple Usage

Query parameters are plain anonymous structs — field names are parameter names; values may be strings, integers, floats, or booleans.

const std = @import("std");
const serpapi = @import("serpapi");

pub fn main(init: std.process.Init) !void {
    const api_key = init.environ_map.get("SERPAPI_KEY") orelse
        return error.MissingSerpApiKey;

    var client = try serpapi.Client.init(init.gpa, .{
        .engine = "google",
        .api_key = api_key,
    });
    defer client.deinit();

    var results = try client.search(.{ .q = "coffee" });
    defer results.deinit();

    std.debug.print("{f}\n", .{std.json.fmt(results.value, .{ .whitespace = .indent_2 })});
}

This example runs a search for "coffee" on Google. It returns the results as a std.json.Parsed(std.json.Value) tree. See the playground to generate your own query.

A complete, runnable version of this program lives in oobt/demo.zig — it prints the title and link of every organic result. Run it against the live API with zig build oobt.

The SerpApi key can be obtained from serpapi.com/signup.

Environment variables are a secure, safe, and easy way to manage secrets: set export SERPAPI_KEY=<secret_serpapi_key> in your shell, and the example above reads it with init.environ_map.get("SERPAPI_KEY") — never hardcode the key in source code.

As everywhere in Zig, the caller owns returned resources: results from JSON APIs are released with deinit(), raw HTML slices with allocator.free(). The remaining examples omit the defer cleanup lines for brevity.

Client options

The serpapi.Client.init constructor takes an allocator and an anonymous struct. timeout and persistent configure the client; every other field becomes a default query parameter applied to every request:

var client = try serpapi.Client.init(allocator, .{
    .api_key = api_key,  // read from the SERPAPI_KEY environment variable
    .engine = "google",  // default search engine
    .gl = "us",          // any other field: default query parameter
    .timeout = 30,       // HTTP timeout in seconds [default: 120]
    .persistent = true,  // keep the connection open [default: true]
});

All fields are optional. Parameters passed to a method call override the defaults provided to the constructor. Call client.deinit() when the client is no longer needed; it closes the connection and frees all resources.

APIs

Search API

var results = try client.search(.{
    .q = "coffee",
    .location = "Austin, TX, Texas, United States",
});
const organic = results.value.object.get("organic_results").?.array;

doc: serpapi.com/search-api

Search API — typed results

Following std.json.parseFromSlice, every JSON method has an As variant that decodes into your own struct instead of a dynamic tree. Unknown JSON fields are ignored, so declare only what you need:

const Answer = struct {
    search_metadata: struct { id: []const u8, status: []const u8 },
};

var results = try client.searchAs(Answer, .{ .q = "coffee" });
std.debug.print("status: {s}\n", .{results.value.search_metadata.status});

Also available: locationAs, searchArchiveAs, and accountAs.

Search API — raw HTML

html returns the raw HTML page from the search engine. It is useful for training AI models, RAG, debugging, or when you need to parse the HTML yourself.

const page = try client.html(.{ .q = "coffee" });

Location API

var locations = try client.location(.{ .q = "Austin", .limit = 3 });

doc: serpapi.com/locations-api

Search Archive API

Retrieve a past search by id — the id comes from results.value.object.get("search_metadata").?.object.get("id").

var archived = try client.searchArchive(search_id);

// or as raw HTML:
const page = try client.searchArchiveHtml(search_id);

doc: serpapi.com/search-archive-api

Account API

var account = try client.account();

The api_key provided to the constructor is used; override it with client.accountAs(std.json.Value, .{ .api_key = "other key" }).

doc: serpapi.com/account-api

Error handling

Methods return a Zig error union. When serpapi.com reports a failure, the call returns error.SerpApiError and the backend message is available from client.errorMessage():

const results = client.search(.{}) catch |err| switch (err) {
    error.SerpApiError => {
        std.debug.print("serpapi.com says: {s}\n", .{client.errorMessage().?});
        return err;
    },
    else => return err,
};

Other errors: error.HttpRequestFailed (non-200 status without an error payload), error.JsonParseError (response was not valid JSON), plus any network / TLS / allocation errors propagated from the standard library.

Search asynchronous

Pass async = true to submit a search without blocking on the result, then fetch it later from the Search Archive API:

var submitted = try client.search(.{ .q = "coffee", .async = true });
const search_id = submitted.value.object.get("search_metadata").?.object.get("id").?.string;

// ... later: poll until search_metadata.status is "Success"
var results = try client.searchArchive(search_id);

Search at scale

With persistent = true (the default), the client keeps the TLS connection to serpapi.com open between requests, which roughly doubles throughput on repeated searches (measure it yourself with zig build bench):

for (queries) |query| {
    var results = try client.search(.{ .q = query });
    defer results.deinit();
    // process results...
}

Developer workflow

zig build test    # run unit tests (no network)
zig build itest   # run integration tests against serpapi.com (needs SERPAPI_KEY)
zig build oobt    # out-of-box testing: build + run the demo app (needs SERPAPI_KEY)
zig build bench   # benchmark persistent vs non-persistent connections (needs SERPAPI_KEY)
zig build cov     # measure code coverage (needs kcov + SERPAPI_KEY)
zig build wasm    # build the browser wasm demo into zig-out/demo-wasm/
zig build serve   # serve the browser wasm demo at http://127.0.0.1:8080 (needs SERPAPI_KEY)
zig build lint    # check formatting (zig fmt --check)
zig build doc     # generate API documentation under zig-out/docs

A Rakefile wraps the same steps for anyone used to the other SerpApi libraries — rake --tasks lists them all.

Cross compilation

Zig cross-compiles without a toolchain to install, so the library builds for every architecture listed in the cross-compilation guide straight from a laptop:

rake cross           # all six architectures
rake cross:aarch64   # or one at a time
architecture zig target verified
x86_64 x86_64-linux ELF 64-bit x86-64
arm arm-linux ELF 32-bit ARM EABI5
aarch64 aarch64-linux ELF 64-bit ARM aarch64
i386 x86-linux ELF 32-bit Intel 80386
riscv64 riscv64-linux ELF 64-bit UCB RISC-V
wasm32 wasm32-wasi WebAssembly module

Note that Zig names the 32-bit x86 architecture x86, not i386.

To run those foreign binaries — and the test suite — on a macOS host, install QEMU and pass -fqemu:

rake install:qemu    # brew install qemu
rake cross:test      # zig build test -Dtarget=<triple> -fqemu

Browser wasm demo

demo/wasm runs part of the client as WebAssembly inside a web page, with a small native server handling what a browser cannot:

export SERPAPI_KEY=<secret_serpapi_key>
zig build serve
# open http://127.0.0.1:8080

See demo/wasm/README.md for how it fits together, the JS/wasm interface, and why the whole serpapi.Client cannot run in a browser.

Code coverage

Coverage is measured with kcov, which Zig binaries support out of the box — no instrumentation flags required.

brew install kcov            # macOS; on Debian/Ubuntu: sudo apt-get install kcov
export SERPAPI_KEY=<secret_serpapi_key>
zig build cov
open zig-out/coverage/merged/kcov-merged/index.html

Current state: 90.4% of lines covered (225 of 249 in src/client.zig) without an API key, since the tests covering html, searchArchive, and account skip themselves rather than fail. With SERPAPI_KEY set — as in CI, which publishes the figure to every job summary — those paths execute too and coverage rises accordingly.

The lines that remain uncovered either way are errdefer branches that only execute if the operating system refuses an allocation midway through a request.

One caveat specific to Zig: a coverage run over the unit tests alone reports a flattering ~97% while actually exercising far less, because Zig never generates code for a generic (anytype) function that no test instantiates — so every HTTP method disappears from the denominator instead of counting as uncovered. zig build cov therefore measures the unit and integration binaries and merges the two reports.

License

MIT License — see LICENSE.

About

Zig client library for SerpApi.com

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages