Skip to content

Make a GET request snippet

Fetch a resource over HTTP with GET — the read verb of the web, used for REST endpoints, health checks, and pulling any payload a server exposes.

Fetch a resource over HTTP with GET — the read verb of the web, used for REST endpoints, health checks, and pulling any payload a server exposes. One trap dominates every language here: HTTP clients report TRANSPORT failure (DNS, TLS, refused connection) through their error channel, but a 404 or 500 is a successful HTTP exchange — the status check is yours to write (res.ok, StatusCode, is_success(), HTTPSuccess, CURLINFO_RESPONSE_CODE). Python's urllib and Ruby's Net::HTTP.get are the exceptions that raise on 4xx/5xx. Zig ships a stdlib client; C and C++ lean on libcurl, the one dependency to name; SQL genuinely cannot do this unaided — vanilla SQL opens no sockets, so Postgres needs the pgsql-http extension. Default timeouts are 'forever' in several of these — that is its own snippet later in this backlog.

Runnable recipe · 15 languages
HTTP & APIshttphttp-clientget-requestrest

Every language

15 implementations, copy-ready. One at a time with syntax highlighting, or all inline.

SQLSQLrunnable
-- PostgreSQL + the community pgsql-http extension:
--   CREATE EXTENSION http;   -- from PGXN or your distro's package
SELECT status,
       content_type,
       left(content, 80) AS body_preview
FROM   http_get('https://jsonplaceholder.typicode.com/posts/1');
--  status |      content_type      | body_preview
--  -------+------------------------+------------------------------------------
--     200 | application/json; ...  | { "userId": 1, "id": 1, "title": "sunt ...

Vanilla SQL opens no sockets — this is where SQL does NOT fit unaided. The Postgres community pgsql-http extension supplies http_get(); Supabase ships it, AWS RDS does not. Its result is ordinary rows (status, content_type, headers, content), so you can JOIN and filter on responses. MySQL/SqLite/SQL Server have no equivalent short of stored procedures shelling out.

Run in the SQL playground →
JSJavaScript
const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const post = await res.json();
console.log(res.status, post.title);

Node 18+ and all browsers ship fetch(). It rejects ONLY on network failure — a 404 resolves happily, so res.ok (any 200–299) is the check you must write yourself. Node's fetch has NO default timeout: pass signal: AbortSignal.timeout(ms) before it hangs forever.

TSTypeScript
interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

const res = await fetch('https://jsonplaceholder.typicode.com/posts/1');
if (!res.ok) throw new Error(`HTTP ${res.status}`); // 404 is NOT a rejection

const post = (await res.json()) as Post; // cast = trust; validate if untrusted
console.log(res.status, post.title);

export {};

Top-level await needs an ESM context ("type": "module" / .mts) — export {} marks the file as a module. res.json() is typed Promise<unknown> in lib.dom, so the `as Post` is trust, not validation; run untrusted bodies through a schema library (zod) instead. Same res.ok trap as plain JS.

GoGo
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	res, err := http.Get("https://jsonplaceholder.typicode.com/posts/1")
	if err != nil {
		panic(err) // transport failure only — a 404 is NOT an error in Go
	}
	defer res.Body.Close() // forgetting this leaks the connection

	if res.StatusCode != http.StatusOK {
		panic(fmt.Sprintf("HTTP %d", res.StatusCode))
	}
	body, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%d %s\n", res.StatusCode, body)
}

http.Get is shorthand for http.DefaultClient, which has NO timeout — fine for a demo, fatal in production (see the timeout snippet). err covers transport only: a 404 returns err == nil with the status on res, and skipping defer res.Body.Close() leaks the connection.

RsRust
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let res = reqwest::blocking::get("https://jsonplaceholder.typicode.com/posts/1")?;
    let status = res.status();
    let body = res.text()?;
    if !status.is_success() {
        return Err(format!("HTTP {status}").into());
    }
    println!("{status} {}", &body[..body.len().min(80)]);
    Ok(())
}

Cargo.toml: reqwest = { version = "0.12", features = ["blocking"] } — Rust has no stdlib HTTP. blocking::get is right for CLIs; servers want the async client under tokio. Like everywhere else, a 404 is Ok — res.error_for_status() is the built-in that converts it to Err.

PHPPHP
<?php
$ch = curl_init('https://jsonplaceholder.typicode.com/posts/1');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true, // without this the body PRINTS and exec returns true
    CURLOPT_FOLLOWLOCATION => true,
]);
$body = curl_exec($ch);
if ($body === false) {
    exit('cURL error: ' . curl_error($ch) . PHP_EOL); // transport failure only
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); // 4xx/5xx arrive here normally
if ($status >= 400) {
    exit("HTTP $status" . PHP_EOL);
}
$post = json_decode($body, true);
echo $status, ' ', $post['title'], PHP_EOL;

ext-curl ships with PHP and is the canonical HTTP story. CURLOPT_RETURNTRANSFER is the flag everyone forgets once — without it the body prints to stdout and curl_exec returns true. Handles self-close since PHP 8: curl_close() is deprecated (8.5) and does nothing. file_get_contents($url) plus $http_response_header is the zero-extension fallback.

PyPython
import json
import urllib.error
import urllib.request

url = "https://jsonplaceholder.typicode.com/posts/1"
try:
    with urllib.request.urlopen(url) as res:  # 4xx/5xx RAISE here
        post = json.load(res)
        print(res.status, post["title"])
except urllib.error.HTTPError as e:
    print("HTTP", e.code)  # e is still a response: e.read() has the body

urllib.request is stdlib — no pip install. THE difference from JS fetch: urlopen RAISES urllib.error.HTTPError on 4xx/5xx — but the exception is itself a response object, so e.read() still holds the server's error body. The requests package is the ergonomic third-party favorite.

CC
#include <stdio.h>
#include <string.h>
#include <curl/curl.h>

typedef struct {
    char data[4096];
    size_t len;
} Body;

/* libcurl delivers the body in chunks — accumulate them yourself */
static size_t write_cb(char *chunk, size_t size, size_t nmemb, void *userdata) {
    Body *body = userdata;
    size_t bytes = size * nmemb;
    size_t room = sizeof(body->data) - body->len - 1;
    size_t fit = bytes < room ? bytes : room;
    memcpy(body->data + body->len, chunk, fit);
    body->len += fit;
    return bytes; /* claim everything, or the transfer aborts */
}

int main(void) {
    curl_global_init(CURL_GLOBAL_DEFAULT);
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    Body body = {0};
    curl_easy_setopt(curl, CURLOPT_URL, "https://jsonplaceholder.typicode.com/posts/1");
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);

    CURLcode rc = curl_easy_perform(curl);
    if (rc != CURLE_OK) { /* transport error — DNS, TLS, timeout */
        fprintf(stderr, "curl: %s\n", curl_easy_strerror(rc));
        curl_easy_cleanup(curl);
        return 1;
    }
    long status = 0; /* a 404 gets here with rc == CURLE_OK — check it */
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
    printf("HTTP %ld %.80s\n", status, body.data);

    curl_easy_cleanup(curl);
    curl_global_cleanup();
    return 0;
}

Compile with `cc main.c -o main -lcurl` — libcurl is the C ecosystem's HTTP answer, there is no stdlib one. The write callback receives the body in ARBITRARY chunks and must return the full byte count or the transfer aborts; 4xx/5xx still report CURLE_OK, so CURLINFO_RESPONSE_CODE is where the truth lands.

C++C++
#include <curl/curl.h>
#include <iostream>
#include <string>

static size_t write_cb(char* chunk, size_t size, size_t nmemb, void* userdata) {
    auto* body = static_cast<std::string*>(userdata);
    body->append(chunk, size * nmemb);
    return size * nmemb; // must return the full count or the transfer aborts
}

int main() {
    curl_global_init(CURL_GLOBAL_DEFAULT);
    CURL* curl = curl_easy_init();
    std::string body;

    curl_easy_setopt(curl, CURLOPT_URL, "https://jsonplaceholder.typicode.com/posts/1");
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);

    if (CURLcode rc = curl_easy_perform(curl); rc != CURLE_OK) {
        std::cerr << "curl: " << curl_easy_strerror(rc) << '\n';
        return 1;
    }
    long status = 0; // 4xx/5xx still arrive as CURLE_OK — inspect the code
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
    curl_easy_cleanup(curl);
    curl_global_cleanup();

    std::cout << status << ' ' << body.substr(0, 80) << '\n';
}

libcurl is a C API — the std::string-append callback is the idiomatic C++ wrapper (cpr wraps libcurl in a native face if you want one). Same two rules as C: always return size * nmemb from the callback, and read CURLINFO_RESPONSE_CODE because CURLE_OK covers 404s.

C#C#
using System.Net.Http;

using var client = new HttpClient(); // fine for a script; services REUSE one instance
using var response = await client.GetAsync("https://jsonplaceholder.typicode.com/posts/1");
response.EnsureSuccessStatusCode(); // throws HttpRequestException on 4xx/5xx

string body = await response.Content.ReadAsStringAsync();
Console.WriteLine($"{(int)response.StatusCode} {body[..Math.Min(80, body.Length)]}");

dotnet run (net6+): top-level statements plus top-level await. HttpClient is thread-safe and meant to be REUSED — one per process for scripts, IHttpClientFactory in ASP.NET Core, never per-request. EnsureSuccessStatusCode() throws on 4xx/5xx; check response.IsSuccessStatusCode when you'd rather branch than throw.

JvJava
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class GetRequest {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
                .GET()
                .build();

        HttpResponse<String> res = client.send(request, HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() != 200) {
            throw new RuntimeException("HTTP " + res.statusCode());
        }
        System.out.println(res.statusCode() + " " + res.body().substring(0, 80));
    }
}

java.net.http.HttpClient lives IN the JDK since 11 — no HttpURLConnection, no dependency. send() blocks the thread; sendAsync() returns a CompletableFuture. Same universal trap: non-2xx is a normal response, not an exception. The client is immutable — build one and reuse it.

SwSwift
import Foundation

struct Post: Decodable {
    let title: String
}

let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!

do {
    let (data, response) = try await URLSession.shared.data(from: url)
    let http = response as! HTTPURLResponse // always one over HTTP(S)
    guard (200..<300).contains(http.statusCode) else {
        throw URLError(.badServerResponse) // URLSession does NOT throw on 4xx/5xx
    }
    let post = try JSONDecoder().decode(Post.self, from: data)
    print(http.statusCode, post.title)
} catch {
    FileHandle.standardError.write("error: \(error)\n".data(using: .utf8)!)
    exit(1)
}

async/await flavor (Swift 5.5+, macOS 12/iOS 15) — call it from any Task or async context; the older dataTask(with:completionHandler:) still works everywhere. URLSession does NOT throw on 4xx/5xx — cast to HTTPURLResponse and read statusCode. data(from:) also enforces App Transport Security: http:// URLs need an Info.plist exception.

KtKotlin
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

fun main() {
    val client = HttpClient.newHttpClient() // JDK 11+ — no extra dependency
    val request = HttpRequest.newBuilder()
        .uri(URI.create("https://jsonplaceholder.typicode.com/posts/1"))
        .build() // GET is the default method

    val response = client.send(request, HttpResponse.BodyHandlers.ofString())
    check(response.statusCode() == 200) { "HTTP ${response.statusCode()}" }

    println("${response.statusCode()} ${response.body().take(80)}")
}

No Kotlin-specific client needed: the JDK 11+ HttpClient is the idiomatic zero-dependency answer on the JVM (this file is `kotlinc`-clean). check{} turns the missing status check into an IllegalStateException carrying the code. ktor-client is the multiplatform option when you target non-JVM too.

RbRuby
require 'net/http'
require 'json'

uri = URI('https://jsonplaceholder.typicode.com/posts/1')
res = Net::HTTP.get_response(uri)

unless res.is_a?(Net::HTTPSuccess)
  abort "HTTP #{res.code}"
end

post = JSON.parse(res.body)
puts "#{res.code} #{post['title']}"

Net::HTTP is stdlib. get_response(uri) returns EVERY status for you to check; the shorter Net::HTTP.get(uri) instead RAISES on non-2xx, and res.value is the explicit raise-here call — pick one and be consistent. URI.open (open-uri) remains the one-liner for body slurps.

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

pub fn main() !void {
    var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
    defer _ = debug_allocator.deinit();
    const allocator = debug_allocator.allocator();

    var client: std.http.Client = .{ .allocator = allocator };
    defer client.deinit();

    var body: std.ArrayList(u8) = .empty; // 0.15: ArrayList is unmanaged
    defer body.deinit(allocator);

    const status = try client.fetch(.{
        .location = .{ .url = "https://jsonplaceholder.typicode.com/posts/1" },
        .response_storage = .{ .dynamic = &body },
    });

    if (status != .ok) return error.UnexpectedStatus;
    std.debug.print("{d} {s}\n", .{ @intFromEnum(status), body.items[0..@min(body.items.len, 80)] });
}

std.http.Client — in the stdlib since 0.11, and honest warning: the API reshuffles almost every release (this is 0.15's DebugAllocator + unmanaged ArrayList + fetch shape), so trust your toolchain's std docs over any snippet. Build with `zig build-exe main.zig`. fetch returns the Status enum; @intFromEnum gives the numeric code.

Keep going

Read the http-methods cheatsheet →