·
16 min read
Write to Fresh Jots from C++ — `cpr`, `cpp-httplib`, or a small SDK
Write to Fresh Jots from C++ — `cpr`, `cpp-httplib`, or a small SDK
A spin-off from Write a note from any project, focused on C++. Three flavors:
- **`cpr` (C++ Requests)** — modern C++ wrapper over libcurl. The recommended path when you have vcpkg/Conan.
- **`cpp-httplib`** — single-header library; drop into builds without package managers.
- **`libcurl` directly** — when you can't add a new dep and `libcurl` is already in your link line.
All three hit the same `/api/v1/notes/by-filename/<name>/append` endpoint. You'll need a Fresh Jots API token (`FRESHJOTS_TOKEN`). If you don't have one, see Get & set your Fresh Jots API token.
No `freshjots` port on vcpkg or Conan yet — the snippets below are MIT-licensed; paste them, adapt them, ship them. C++ has no standard HTTP client (yet), so every approach picks a different transport — the wrapper class shape stays the same.
1. `cpr` — the recommended path (C++17+)
1. `cpr` — the recommended path (C++17+)
```cpp
// freshjots.hpp — single header, C++17+
#pragma once
#include <cpr/cpr.h>
#include <nlohmann/json.hpp>
#include <cstdlib>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
namespace freshjots {
inline constexpr auto kBaseUrl = "https://freshjots.com/api/v1";
namespace detail {
// RFC 3986 unreserved-set percent-encoder. None of the transports
// auto-encode path segments, so a filename like "deploy log" would
// otherwise break the URL.
inline std::string url_encode(const std::string& s) {
static constexpr char hex[] = "0123456789ABCDEF";
std::string out;
out.reserve(s.size() * 3);
for (unsigned char c : s) {
if ((c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') ||
c == '-' || c == '_' || c == '.' || c == '~') {
out += static_cast<char>(c);
} else {
out += '%';
out += hex[c >> 4];
out += hex[c & 0x0F];
}
}
return out;
}
} // namespace detail
class ApiError : public std::runtime_error {
public:
int status;
std::string code;
ApiError(int s, std::string c, const std::string& msg)
: std::runtime_error(msg),
status(s),
code(std::move(c)) {}
};
class Client {
public:
explicit Client(std::optional<std::string> token = std::nullopt) {
if (token) {
token_ = std::move(*token);
} else if (const char* env = std::getenv("FRESHJOTS_TOKEN"); env && *env) {
token_ = env;
} else {
throw std::runtime_error("FRESHJOTS_TOKEN not set");
}
}
void append(const std::string& filename, const std::string& text) const {
const nlohmann::json body = {{"text", text}};
const auto url =
std::string(kBaseUrl) + "/notes/by-filename/" +
detail::url_encode(filename) + "/append";
auto res = cpr::Post(
cpr::Url{url},
cpr::Bearer{token_},
cpr::Header{{"Content-Type", "application/json"}},
cpr::Body{body.dump()});
if (res.status_code >= 400) {
throw parse_error(res);
}
}
private:
static ApiError parse_error(const cpr::Response& res) {
const int status = static_cast<int>(res.status_code);
try {
const auto j = nlohmann::json::parse(res.text);
if (j.is_object() && j.contains("error") && j["error"].is_object()) {
const auto& err = j["error"];
return ApiError(
status,
err.value("code", std::string{"unknown"}),
err.value("message", std::string{"request failed"}));
}
} catch (...) {
// fall through to body-as-message
}
return ApiError(status, "unknown", res.text);
}
std::string token_;
};
} // namespace freshjots
```
Two-line use:
```cpp
#include "freshjots.hpp"
freshjots::Client fj;
fj.append("deploy-log", "deploy ok — sha=abc123");
```
CMake (vcpkg or Conan toolchain):
```cmake
find_package(cpr CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE cpr::cpr nlohmann_json::nlohmann_json)
```
vcpkg:
```sh
vcpkg install cpr nlohmann-json
```
Two design notes:
- **Default `value<std::string>` over `_json_pointer` UDL** — passing `std::string{"unknown"}` as the default forces `T = std::string` via template deduction, so the returned value is owned. The terser-looking `_json_pointer` literal needs a `using namespace nlohmann::literals;` somewhere and returns `const char*` by default, which can dangle.
- **Default `value<std::string>` over `_json_pointer` UDL** — passing `std::string{"unknown"}` as the default forces `T = std::string` via template deduction, so the returned value is owned. The terser-looking `_json_pointer` literal needs a `using namespace nlohmann::literals;` somewhere and returns `const char*` by default, which can dangle.
- **`j.is_object()` before `j.contains(...)`** — `contains` on a non-object throws `json::type_error`. The outer catch handles it, but the explicit guard avoids the throw on every malformed-error-page response.
2. `cpp-httplib` — single-header, no package manager
2. `cpp-httplib` — single-header, no package manager
When you can't pull from vcpkg or Conan, grab `cpp-httplib`'s single header and `nlohmann/json`'s single header, drop them in `third_party/`, and you're done:
```cpp
// freshjots_lite.hpp — header-only, C++17+
// CPPHTTPLIB_OPENSSL_SUPPORT must be defined at build time (see flags below),
// NOT in this header — define-in-header is fragile across translation units.
#pragma once
#include <httplib.h>
#include <nlohmann/json.hpp>
#include <cstdlib>
#include <stdexcept>
#include <string>
class FreshJots {
public:
explicit FreshJots(std::string token = "")
: token_(token.empty() ? require_env("FRESHJOTS_TOKEN") : std::move(token)),
client_("https://freshjots.com") {}
void append(const std::string& filename, const std::string& text) {
const nlohmann::json body = {{"text", text}};
const auto path = "/api/v1/notes/by-filename/" + url_encode(filename) + "/append";
auto res = client_.Post(
path,
{{"Authorization", "Bearer " + token_}},
body.dump(),
"application/json");
if (!res || res->status >= 400) {
throw std::runtime_error(
"Fresh Jots: status=" + std::to_string(res ? res->status : -1));
}
}
private:
static std::string require_env(const char* name) {
const char* v = std::getenv(name);
if (!v || !*v) throw std::runtime_error(std::string(name) + " not set");
return v;
}
// RFC 3986 unreserved-set percent-encoder; cpp-httplib doesn't auto-encode path segments.
static std::string url_encode(const std::string& s) {
static constexpr char hex[] = "0123456789ABCDEF";
std::string out;
out.reserve(s.size() * 3);
for (unsigned char c : s) {
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
c == '-' || c == '_' || c == '.' || c == '~') {
out += static_cast<char>(c);
} else {
out += '%';
out += hex[c >> 4];
out += hex[c & 0x0F];
}
}
return out;
}
std::string token_;
httplib::Client client_;
};
```
Build flags (HTTPS needs OpenSSL):
```sh
g++ -std=c++17 -DCPPHTTPLIB_OPENSSL_SUPPORT main.cpp -lssl -lcrypto
```
The `-DCPPHTTPLIB_OPENSSL_SUPPORT` *must* go in the build, not in the header — `cpp-httplib`'s ABI changes based on that flag, and a header-level define means TUs that include the header *before* it gets seen will silently compile a non-OpenSSL build. On Windows you can swap the OpenSSL flags for the bundled WinHTTP path; check the library README.
Reach for this when the build environment is hostile to package managers (legacy codebases, vendor toolchains, embedded SDKs).
3. `libcurl` — when you can't add anything new
3. `libcurl` — when you can't add anything new
When the build already links libcurl and you don't want a wrapper, this is the verbose-but-zero-new-deps path:
```cpp
#include <curl/curl.h>
#include <nlohmann/json.hpp>
#include <cstdlib>
#include <stdexcept>
#include <string>
class FreshJots {
public:
explicit FreshJots(std::string token = "") {
if (!token.empty()) {
token_ = std::move(token);
} else if (const char* env = std::getenv("FRESHJOTS_TOKEN"); env && *env) {
token_ = env;
} else {
throw std::runtime_error("FRESHJOTS_TOKEN not set");
}
curl_global_init(CURL_GLOBAL_DEFAULT);
}
~FreshJots() { curl_global_cleanup(); }
void append(const std::string& filename, const std::string& text) {
const nlohmann::json body_json = {{"text", text}};
const std::string body = body_json.dump();
const std::string url =
"https://freshjots.com/api/v1/notes/by-filename/" + url_encode(filename) + "/append";
const std::string auth = "Authorization: Bearer " + token_;
CURL* curl = curl_easy_init();
if (!curl) throw std::runtime_error("curl_easy_init failed");
curl_slist* headers = nullptr;
headers = curl_slist_append(headers, auth.c_str());
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
long status = 0;
CURLcode res_code = curl_easy_perform(curl);
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (res_code != CURLE_OK) {
throw std::runtime_error(std::string("curl: ") + curl_easy_strerror(res_code));
}
if (status >= 400) {
throw std::runtime_error("Fresh Jots: HTTP " + std::to_string(status));
}
}
private:
// RFC 3986 unreserved-set percent-encoder.
static std::string url_encode(const std::string& s) {
static constexpr char hex[] = "0123456789ABCDEF";
std::string out;
out.reserve(s.size() * 3);
for (unsigned char c : s) {
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
c == '-' || c == '_' || c == '.' || c == '~') {
out += static_cast<char>(c);
} else {
out += '%';
out += hex[c >> 4];
out += hex[c & 0x0F];
}
}
return out;
}
std::string token_;
};
```
A few things worth knowing about this snippet:
- **`curl_global_init` is reference-counted**, so the constructor/destructor pattern is safe for multiple `FreshJots` instances. But libcurl's docs caution that the first `curl_global_init` isn't thread-safe — for multi-threaded apps that also use libcurl elsewhere, hoist the init/cleanup pair to `main`.
- **The snippet leaks `headers` and `curl` if an exception escapes between `_init` and `_cleanup`.** All current code paths complete cleanup before any throw, but a `std::bad_alloc` from a `std::string` operation could escape. For production code, wrap the handles in `std::unique_ptr` with custom deleters (`std::unique_ptr<CURL, decltype(&curl_easy_cleanup)>{curl, &curl_easy_cleanup}`) to get RAII cleanup automatically.
Reach for this version when you're already linking `-lcurl` and adding `cpr` would mean introducing a new C++ dep into a mostly-C codebase.
4. Branching on errors
4. Branching on errors
The `cpr` and `cpp-httplib` paths throw `freshjots::ApiError` (or `std::runtime_error` for `cpp-httplib`'s simpler version) with a parsed `code`. Switch on it:
```cpp
try {
fj.append("deploy-log", entry);
} catch (const freshjots::ApiError& e) {
if (e.code == "rate_limited") {
std::this_thread::sleep_for(std::chrono::minutes(1));
fj.append("deploy-log", entry);
} else if (e.code == "cap_exceeded" || e.code == "storage_cap_exceeded") {
ops_alert("Fresh Jots " + e.code + "; dropping log entry.");
} else if (e.code == "unauthenticated" || e.code == "forbidden") {
throw std::runtime_error("token bad — rotate at /settings/api_tokens");
} else {
throw;
}
}
```
C++23 `std::expected` gives you a throw-free alternative when exceptions aren't allowed (RTOS, kernel-adjacent, game engines compiled with `-fno-exceptions`):
```cpp
[[nodiscard]] std::expected<void, ApiError> append_safe(
const std::string& filename, const std::string& text) noexcept;
```
GCC 13+, Clang 17+, MSVC 19.36+ ship it. For older compilers, return `std::variant<std::monostate, ApiError>` or a small `Result` struct.
5. Patterns that work
5. Patterns that work
A. Background heartbeat thread
```cpp
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <thread>
class Heartbeat {
public:
Heartbeat(freshjots::Client& fj, std::chrono::seconds interval)
: fj_(fj), interval_(interval), thread_([this] { run(); }) {}
~Heartbeat() {
{
std::lock_guard lock(mtx_);
stop_ = true;
}
cv_.notify_all();
if (thread_.joinable()) thread_.join();
}
private:
void run() {
std::unique_lock lock(mtx_);
while (!stop_) {
lock.unlock();
try {
fj_.append("worker-heartbeat", "alive");
} catch (const std::exception&) {
// swallow — never let a heartbeat failure kill the loop
}
lock.lock();
cv_.wait_for(lock, interval_, [this] { return stop_; });
}
}
freshjots::Client& fj_;
std::chrono::seconds interval_;
bool stop_{false};
std::mutex mtx_;
std::condition_variable cv_;
std::thread thread_;
};
// Use:
freshjots::Client fj;
Heartbeat hb(fj, std::chrono::minutes(15)); // dtor handles clean shutdown
```
The `condition_variable` is the load-bearing piece: a plain `std::this_thread::sleep_for` would block destruction for up to the full interval. The lock dance — unlock for the network call, re-lock before `wait_for` — keeps the mutex contention minimal while still serializing access to `stop_`. Pair this with `append_deadline_hours: 1` on the `worker-heartbeat` note: Fresh Jots emails you within an hour if the heartbeat goes silent. See [Everything you can do here](/blog/everything-you-can-do-here).
If you're on C++20, `std::jthread` + `std::stop_token` replaces the manual stop flag and the explicit join:
```cpp
std::jthread t([&fj](std::stop_token st) {
while (!st.stop_requested()) {
try { fj.append("worker-heartbeat", "alive"); } catch (...) {}
std::this_thread::sleep_for(std::chrono::minutes(15)); // or a stop-aware sleep
}
});
// jthread auto-requests stop and joins on destruction
```
B. Async append with `std::async`
```cpp
auto fut = std::async(std::launch::async, [&fj] {
fj.append("background-job", "started");
});
// ... do other work ...
fut.get(); // joins, propagates exception if any
```
For a one-off fire-and-forget log, drop the `.get()` — but then exceptions become silent (and dropping the future blocks on its destructor if it came from `std::async`, defeating the point). Prefer a thread-pool / channel pattern over `std::async` for high-frequency calls.
C. Qt — `QNetworkAccessManager`
C. Qt — `QNetworkAccessManager`
```cpp
class FreshJots : public QObject {
public:
explicit FreshJots(QObject* parent = nullptr)
: QObject(parent), token_(qEnvironmentVariable("FRESHJOTS_TOKEN")) {}
void append(const QString& filename, const QString& text) {
const QString encoded = QString::fromUtf8(QUrl::toPercentEncoding(filename));
const QUrl url("https://freshjots.com/api/v1/notes/by-filename/" + encoded + "/append");
QNetworkRequest req(url);
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
req.setRawHeader("Authorization", ("Bearer " + token_).toUtf8());
QJsonObject body{{"text", text}};
nam_.post(req, QJsonDocument(body).toJson());
}
private:
QNetworkAccessManager nam_;
QString token_;
};
```
Qt's `QNetworkAccessManager` is event-loop-driven; the call returns immediately and emits `finished(QNetworkReply*)` on completion. Idiomatic for any Qt app — connect the signal to a slot that handles the response. `QUrl::toPercentEncoding` is the Qt idiom for path-segment encoding.
D. Unreal Engine — `FHttpModule`
D. Unreal Engine — `FHttpModule`
C++ inside Unreal doesn't use the standard library for HTTP; it uses `FHttpModule`. The wrapper-class pattern still applies: write a small `UFreshJots` class exposing `Append(FString filename, FString text)` that builds an `IHttpRequest`, sets the bearer header, and posts. Same surface, different transport.
E. Embedded — `-fno-exceptions`, `-fno-rtti`
E. Embedded — `-fno-exceptions`, `-fno-rtti`
For builds that disable exceptions (RTOS, AUTOSAR, MISRA), return a result code instead of throwing:
```cpp
enum class AppendStatus { Ok, TokenMissing, NetworkFailed, ApiError };
[[nodiscard]] AppendStatus append(const char* filename, const char* text) noexcept;
```
No JSON parsing (skip nlohmann), no exceptions, no dynamic allocation in the hot path. The transport in embedded contexts is usually a vendor-provided HTTPS stack (lwIP + mbedTLS, ESP-IDF, etc.) — not libcurl.
6. Loading the token
6. Loading the token
- **Shell profile** (`~/.bashrc`, `~/.zshrc`, Windows env vars) — see Get & set your Fresh Jots API token. Best for local dev.
- **`.env` + a dotenv loader** (e.g. [dotenv-cpp](https://github.com/laserpants/dotenv-cpp)) — project-scoped tokens. Add `.env` to `.gitignore`.
- **CMake-time injection** via `target_compile_definitions` — **don't** bake personal tokens into the binary this way; it's fine for CI fixtures, but the binary becomes a token-carrier.
- **Container / orchestrator secret** — Kubernetes `Secret`, Docker secrets, systemd `EnvironmentFile=` with `LoadCredential=`. Always for production.
`std::getenv` returns `nullptr` for unset vars on every platform — always check before dereferencing. Windows additionally has `_dupenv_s` if you want the secure-CRT variant.
7. Going further
7. Going further
- **`cpr` documentation:** [docs.libcpr.org](https://docs.libcpr.org/)
- **`cpp-httplib` on GitHub:** [github.com/yhirose/cpp-httplib](https://github.com/yhirose/cpp-httplib)
- **`libcurl` easy interface:** [curl.se/libcurl/c/libcurl-easy.html](https://curl.se/libcurl/c/libcurl-easy.html)
- **`nlohmann/json`:** [json.nlohmann.me](https://json.nlohmann.me/)
- **Other languages — same pattern, different HTTP client.** Hub: Write a note from any project.
- **The CLI** — for shell-from-C++ via `popen` or `boost::process`. See Notes from your terminal.
- **Dead-man alerts** — pair the heartbeat pattern with `append_deadline_hours`. See Everything you can do here.
One method, one Authorization header, one note in your account — pasted into a worker today, alerting you about silent processes tomorrow.