Introduction Link to heading

This is part 2 of the ongoing series to implement a basic weather CLI using Zig and tomorrow.io. The final result will take in a location and return weather data and format it with colors and emojis. You can read part 1 here

zweather

Note: The final code for this series is complete and lives in the git repo here. Feel free to take a look and if you want to look at the code yourself

Picking Up Where We Left Off Link to heading

From the last part we should have our CLI in a state where we can pass in both the location (-l) and the units (-u). We also read in our API key and on the initial run, the program will check for the existence of the TOMORROW_API_KEY environment variable. Our CLI currently will just print this information out and that is it.

We want to now make our HTTP request to the tomorrow.io API endpoint to fetch the weather information from the flags we pass in!

Setting Up HTTP GET Request Link to heading

The Zig std http module is how we will make this request. We are going to be initializing a Client and using it to build our request and receive our response.

A quick thing we want to do at the top is defined a const http which will point to the std.http module. This way we can write a little bit less on each line.

const std = @import("std");
const http = std.http;
const Chameleon = @import("chameleon");
const clap = @import("clap");

Now let’s create a new function to do this API call

fn fetchWeatherData(app: App) !void {
    var client = http.Client{ .allocator = app.allocator };
    defer client.deinit();
}

This setups up a simple Client object using the allocator we have already defined to handle any dynamic memory allocation that the http module needs.

Next step is to setup the URL we are going to use. For our case we are going to be using the realtime weather API endpoint. We can see from the documentation that we need to provide the location and API key. We also are going to be adding in the units as well. Let’s set that up.

fn fetchWeatherData(app: App) !void {
    var client = http.Client{ .allocator = app.allocator };
    defer client.deinit();

    const uriString = "https://docs.tomorrow.io/reference/realtime-weather?location={s}&units{s}&apikey={s}";
    const url = try std.fmt.allocPrint(app.allocator, uriString, . {app.location, app.units. app.apiKey});
    defer app.allocator.free(url);
}

We are doing something a bit interesting here. We are creating a template string and then using the allocPrint function to feed our flags into a new url variable!

Not we are going to do a few important steps. We want to:

  1. Convert the URL string to a URI object which the client.open() function takes an as argument
  2. Create our header buffer which will be empty in our case but we still need to define
  3. Make the http request
  4. Read that response

Let’s break it down line by line

fn fetchWeatherData(app: App) !void {
    var client = http.Client{ .allocator = app.allocator };
    defer client.deinit();

    const uriString = "https://docs.tomorrow.io/reference/realtime-weather?location={s}&units{s}&apikey={s}";
    const url = try std.fmt.allocPrint(app.allocator, uriString, . {app.location, app.units. app.apiKey});
    defer app.allocator.free(url);

    // This will parse our []const u8 (string) into a URI
    const uri = try std.Uri.parse(url);

    // We define our header buffer as an empty slice
    const serverHeaderBuffer: []u8 = try app.allocator.alloc(u8, 1024 * 8);
    defer app.allocator.free(serverHeaderBuffer);

    // Make the connection to the server.
    var req = try client.open(.GET, uri, .{
        .server_header_buffer = serverHeaderBuffer,
    });
    defer req.deinit();

    // we need to run these 3 functions in order to send and read our resulting values
    try req.send();
    try req.finish();
    try req.wait();

    // read the response from the server into a dynamic memory location
    const body = try req.reader().readAllAlloc(app.allocator, 1024 * 8);
    defer app.allocator.free(body);

    std.debug.print("Response from tomorrow.io: {s}", .{body});
}

Okay most of this should seem straight forward, we use the URI module to parse our string into the correct value that the client.open() function expects. After that we define a header buffer. This is simply an empty slice that we pass into our request for the headers to be stored in.

The most interesting piece here is the sequence of send, finish, and wait function calls. If you peek at the documentation for the Request struct you will see the following definitions of those three functions.

pub fn send(req: *Request) SendError!void

Send the HTTP request headers to the server.

pub fn finish(req: *Request) FinishError!void

Finish the body of a request. This notifies the server that you have no more data to send. Must be called after send.

pub fn wait(req: *Request) WaitError!void

Waits for a response from the server and parses any headers that are sent. This function will block until the final response is received.

We need to execute these three functions in order to get a good response from the server. After we call the wait function we then invoke readAllAlloc function to store the information into the body variable.

Now we only have one more thing to do and that is add this call to the end of our main function and give it shot!

pub fn main() !void {
    ...
    .

    try fetchWeatherData(app);
}
zweather on master [!?] via ↯ v0.14.0 
[Docker] ❯ zig build run -- -l "new york" -u "imperial"
Response from tomorrow.io: {"data":{"time":"2025-04-17T20:47:00Z","values":{"cloudBase":9.9,"cloudCeiling":9.9,"cloudCover":1,"dewPoint":27.8,"freezingRainIntensity":0,"humidity":51,"precipitationProbability":0,"pressureSeaLevel":30.19,"pressureSurfaceLevel":29.77,"rainIntensity":0,"sleetIntensity":0,"snowIntensity":0,"temperature":44.8,"temperatureApparent":44.8,"uvHealthConcern":0,"uvIndex":1,"visibility":9.94,"weatherCode":1000,"windDirection":289,"windGust":21.3,"windSpeed":12.4}},"location":{"lat":43.15616989135742,"lon":-75.8449935913086,"name":"New York, United States","type":"administrative"}}

zweather on master [!?] via ↯ v0.14.0 
[Docker] ❯ zig build run -- -l "new york" -u "metric"  
Response from tomorrow.io: {"data":{"time":"2025-04-17T20:47:00Z","values":{"cloudBase":16,"cloudCeiling":16,"cloudCover":1,"dewPoint":-2.4,"freezingRainIntensity":0,"humidity":51,"precipitationProbability":0,"pressureSeaLevel":1022.31,"pressureSurfaceLevel":1008.26,"rainIntensity":0,"sleetIntensity":0,"snowIntensity":0,"temperature":7.1,"temperatureApparent":7.1,"uvHealthConcern":0,"uvIndex":1,"visibility":16,"weatherCode":1000,"windDirection":289,"windGust":9.5,"windSpeed":5.6}},"location":{"lat":43.15616989135742,"lon":-75.8449935913086,"name":"New York, United States","type":"administrative"}}

This is perfect! We can see that our request worked perfectly, and that changing between imperial and metric responses works as well!

In the final step for this part we want to setup our JSON module to parse this response and store it into a struct. Let’s get into it!

Setting Up Our Response Structs Link to heading

This is arguably the most tedious part of this entire process. We want to define our struct to match the response object defined in the API documentation.

At the top near our App definition, let’s add this response structure.

pub const Root = struct {
    data: Data,
    location: Location,
};

pub const Data = struct {
    time: []u8,
    values: Values,
};

pub const Location = struct {
    lat: f64,
    lon: f64,
    name: []u8,
    type: []u8,
};

pub const Values = struct {
    cloudBase: ?f64,
    cloudCeiling: ?f64,
    cloudCover: ?f64,
    dewPoint: ?f64,
    freezingRainIntensity: ?f64,
    humidity: ?f64,
    precipitationProbability: ?f64,
    pressureSeaLevel: ?f64,
    pressureSurfaceLevel: ?f64,
    rainIntensity: ?f64,
    sleetIntensity: ?f64,
    snowIntensity: ?f64,
    temperature: ?f64,
    temperatureApparent: ?f64,
    uvHealthConcern: ?f64,
    uvIndex: ?f64,
    visibility: ?f64,
    weatherCode: ?u32,
    windDirection: ?f64,
    windGust: ?f64,
    windSpeed: ?f64,
};

You can see that I broke up each object block into its own struct. You can see the structure is quite simple. We have two main blocks. Our data consists mainly of the actual weather data reported, and the location consists of information related to the place we are looking up.

Now that we have the structure of the response defined, we can use the JSON module and parse it into our object!

Just like the http module we also want to define json.

const std = @import("std");
const json = std.json;
const http = std.http;
const Chameleon = @import("chameleon");
const clap = @import("clap");

Now in our fetchWeatherData function we want to replace the print line at the bottom with the following:

fn fetchWeatherData(app: App) !void {
    ...
    .
    .
    const parsed = try json.parseFromSlice(Root, app.allocator, body, .{});
    defer parsed.deinit();

    try json.stringify(parsed.value, .{ .whitespace = .indent_4 }, std.io.getStdOut().writer());
}

Now we are taking the response body from earlier and parsing it into our Root struct which will populate the fields with the matching names! Pretty cool eh?

The final line here simple called the stringify method which allows us to print the parsed JSON object out so we can verify things look good!

zweather on master [!?] via ↯ v0.14.0 took 9s 
[Docker] ❯ zig build run -- -l "new york" -u "imperial"
{
    "data": {
        "time": "2025-04-17T21:15:00Z",
        "values": {
            "cloudBase": 9.9e0,
            "cloudCeiling": 9.9e0,
            "cloudCover": 0e0,
            "dewPoint": 2.89e1,
            "freezingRainIntensity": 0e0,
            "humidity": 5.3e1,
            "precipitationProbability": 0e0,
            "pressureSeaLevel": 3.019e1,
            "pressureSurfaceLevel": 2.978e1,
            "rainIntensity": 0e0,
            "sleetIntensity": 0e0,
            "snowIntensity": 0e0,
            "temperature": 4.51e1,
            "temperatureApparent": 4.51e1,
            "uvHealthConcern": 0e0,
            "uvIndex": 1e0,
            "visibility": 9.94e0,
            "weatherCode": 1000,
            "windDirection": 2.88e2,
            "windGust": 1.83e1,
            "windSpeed": 7.2e0
        }
    },
    "location": {
        "lat": 4.315616989135742e1,
        "lon": -7.58449935913086e1,
        "name": "New York, United States",
        "type": "administrative"
    }
}

Amazing! We now have a functional CLI that we can query weather information for and return the response to the user! We have made great progress in this section.

End of Part 2 Link to heading

In the next block we are going to work towards making our CLI look nice with colors and emojis based on the temperature and the weather!

We only have one more section to tackle! If you want to keep in touch I have my socials listed on the homepage, and you can subscribe to my RSS feed