·
12 min read
Write to Fresh Jots from Java — `HttpClient`, OkHttp, or a small SDK
Write to Fresh Jots from Java — `HttpClient`, OkHttp, or a small SDK
A spin-off from Write a note from any project, focused on Java. Three flavors:
- **JDK 11+ `HttpClient`** — zero-dependency, stdlib HTTP. The recommended path.
- **OkHttp** — when you're already shipping it (Android, server-side, Square stack).
- **Spring `RestClient`** — Boot 3.2+, idiomatic for `@Service` injection.
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` package on Maven Central yet — the snippets below are MIT-licensed; paste them, adapt them, ship them. An official Java client may follow once usage signals it's worth maintaining.
1. `HttpClient` — the recommended path (JDK 11+)
1. `HttpClient` — the recommended path (JDK 11+)
```java
// FreshJots.java — single file, JDK 11+, zero deps
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
public final class FreshJots {
private static final String BASE = "https://freshjots.com/api/v1";
private final String token;
private final HttpClient http;
public FreshJots() {
this(System.getenv("FRESHJOTS_TOKEN"));
}
public FreshJots(String token) {
if (token == null || token.isBlank()) {
throw new IllegalStateException("FRESHJOTS_TOKEN not set");
}
this.token = token;
this.http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
}
public void append(String filename, String text) throws Exception {
var encoded = URLEncoder.encode(filename, StandardCharsets.UTF_8);
var body = "{\"text\":" + jsonString(text) + "}";
var req = HttpRequest.newBuilder(URI.create(BASE + "/notes/by-filename/" + encoded + "/append"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(30))
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();
var res = http.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 400) {
throw new ApiError(res.statusCode(), res.body());
}
}
private static String jsonString(String s) {
var sb = new StringBuilder("\"");
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '"' -> sb.append("\\\"");
case '\\' -> sb.append("\\\\");
case '\n' -> sb.append("\\n");
case '\r' -> sb.append("\\r");
case '\t' -> sb.append("\\t");
default -> {
if (c < 0x20) sb.append(String.format("\\u%04x", (int) c));
else sb.append(c);
}
}
}
return sb.append('"').toString();
}
public static final class ApiError extends RuntimeException {
public final int status;
public final String body;
public ApiError(int status, String body) {
super("Fresh Jots API " + status + ": " + body);
this.status = status;
this.body = body;
}
}
}
```
Two-line use:
```java
new FreshJots().append("deploy-log", "deploy ok — sha=abc123");
```
That's the entire dependency surface. No `pom.xml` edits, no Jackson, no OkHttp — just JDK 11+ and your code. The JSON encoder is hand-rolled because for a single `{"text": "..."}` body it's seven lines; pulling Jackson in for that one call would dwarf the actual code.
Async — fire-and-forget from a hot path
```java
http.sendAsync(req, HttpResponse.BodyHandlers.ofString())
.thenAccept(res -> {
if (res.statusCode() >= 400) log.warn("FJ {}: {}", res.statusCode(), res.body());
});
```
`HttpClient` is async-native. On a request hot path (controller, GraphQL resolver, hot Kafka consumer), use `sendAsync` so the log call doesn't block the response.
2. OkHttp
2. OkHttp
When you're already shipping OkHttp (Android apps, Square-stack services), reuse the connection pool:
```java
// FreshJotsOk.java
import okhttp3.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
import java.io.IOException;
public class FreshJotsOk {
private static final String BASE = "https://freshjots.com/api/v1";
private static final MediaType JSON = MediaType.get("application/json");
private final String token;
private final OkHttpClient client;
private final ObjectMapper mapper = new ObjectMapper();
public FreshJotsOk(String token, OkHttpClient client) {
this.token = token;
this.client = client;
}
public void append(String filename, String text) throws IOException {
var bodyJson = mapper.writeValueAsString(Map.of("text", text));
var req = new Request.Builder()
.url(BASE + "/notes/by-filename/" + filename + "/append")
.header("Authorization", "Bearer " + token)
.post(RequestBody.create(bodyJson, JSON))
.build();
try (var res = client.newCall(req).execute()) {
if (!res.isSuccessful()) {
var snippet = res.body() == null ? "" : res.body().string();
throw new IOException("Fresh Jots " + res.code() + ": " + snippet);
}
}
}
}
```
Maven:
```xml
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
```
Reach for OkHttp when you already have it. Don't add it just for one API call — the JDK `HttpClient` above handles that case with zero new dependencies and the same async ergonomics.
3. Spring `RestClient` (Boot 3.2+)
3. Spring `RestClient` (Boot 3.2+)
```java
// FreshJotsService.java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import java.util.Map;
@Service
public class FreshJotsService {
private final RestClient client;
public FreshJotsService(@Value("${freshjots.token}") String token) {
this.client = RestClient.builder()
.baseUrl("https://freshjots.com/api/v1")
.defaultHeader("Authorization", "Bearer " + token)
.build();
}
public void append(String filename, String text) {
client.post()
.uri("/notes/by-filename/{f}/append", filename)
.contentType(MediaType.APPLICATION_JSON)
.body(Map.of("text", text))
.retrieve()
.toBodilessEntity();
}
}
```
`application.properties`:
```properties
freshjots.token=${FRESHJOTS_TOKEN}
```
`@Value("${freshjots.token}")` resolves from the system env via Spring's standard property chain. In production the env var ships from your container/orchestrator (Kubernetes `Secret`, ECS task `secrets:`, Cloud Run revision env) — same pattern as every other language.
4. Branching on errors with sealed types
4. Branching on errors with sealed types
Java 17+ sealed interfaces + Java 21 record patterns give you exhaustive error switches without the `instanceof`-ladder ceremony of earlier versions:
```java
public sealed interface FjResult permits FjResult.Ok, FjResult.Err {
record Ok(int status) implements FjResult {}
record Err(int status, String code, String message) implements FjResult {}
}
// Pattern-matching switch (Java 21+)
switch (result) {
case FjResult.Ok ok -> log.info("appended");
case FjResult.Err(int s, String code, var msg) when code.equals("rate_limited") -> retryAfter(60);
case FjResult.Err(int s, String code, var msg) when code.startsWith("cap_") -> opsAlert(code);
case FjResult.Err(int s, String code, var msg) when code.equals("unauthenticated") ->
throw new IllegalStateException("token bad — rotate at /settings/api_tokens");
case FjResult.Err err -> throw new RuntimeException(err.code() + ": " + err.message());
}
```
The compiler enforces exhaustiveness. Add a new `code` value to the API later and every `switch` over `FjResult` that doesn't handle it lights up red in your IDE. Pin error handling at the type level, not at runtime.
If you're on Java 11–17, the same idea works with `instanceof` patterns — just verbose:
```java
if (result instanceof FjResult.Err err && err.code().equals("rate_limited")) { ... }
```
5. Patterns that work
5. Patterns that work
A. Spring Boot — `@Scheduled` heartbeat with a dead-man alert
```java
@Service
public class HeartbeatJob {
private final FreshJotsService freshJots;
private static final Logger log = LoggerFactory.getLogger(HeartbeatJob.class);
public HeartbeatJob(FreshJotsService freshJots) { this.freshJots = freshJots; }
@Scheduled(fixedRate = 15 * 60_000) // every 15 min
public void heartbeat() {
try {
freshJots.append("worker-heartbeat", "alive — " + Instant.now());
} catch (Exception e) {
log.warn("heartbeat skipped: {}", e.getMessage());
}
}
}
```
Pair this with `append_deadline_hours: 1` on the `worker-heartbeat` note in the Fresh Jots UI. When the worker dies or gets stuck, Fresh Jots emails you within an hour — no extra monitoring stack needed. See Everything you can do here.
B. Quarkus — `@RegisterRestClient` typed interface
B. Quarkus — `@RegisterRestClient` typed interface
```java
@Path("/api/v1")
@RegisterRestClient(configKey = "freshjots-api")
@ClientHeaderParam(name = "Authorization", value = "Bearer ${freshjots.token}")
public interface FreshJotsClient {
@POST
@Path("/notes/by-filename/{filename}/append")
@Consumes(MediaType.APPLICATION_JSON)
void append(@PathParam("filename") String filename, AppendBody body);
record AppendBody(String text) {}
}
```
`application.properties`:
```properties
quarkus.rest-client.freshjots-api.url=https://freshjots.com/api/v1
freshjots.token=${FRESHJOTS_TOKEN}
```
MicroProfile REST Client turns the interface into a CDI bean — `@Inject FreshJotsClient client;` and call it. Quarkus's native-image build keeps the resulting binary tiny.
C. Kotlin — same JVM, half the code
C. Kotlin — same JVM, half the code
```kotlin
val fj = FreshJots() // reuse the Java class above from Kotlin
fj.append("kotlin-cron", "ok")
```
Or hand-roll in idiomatic Kotlin with `HttpURLConnection` or `ktor-client` — Fresh Jots is one POST and one Authorization header; no client library buys much beyond syntax sugar.
D. Android — proxy through your own backend
D. Android — proxy through your own backend
Same caveat as the [TypeScript post](/blog/freshjots-notes-for-typescript)'s browser section: **don't bake your personal API token into an Android app**. APK reverse-engineering is trivial; anyone running your app can extract the token and get full read-write on your notes.
Two safe patterns:
- **Server proxy.** Your Android app calls *your* backend, your backend calls Fresh Jots. Token lives on the server, never on the device.
- **Per-user tokens (Team tier).** Each end-user mints their own token; the app holds *their* token. Compromising one device leaks only that user's notes.
With either pattern in place, OkHttp from Android is straightforward — same code as the OkHttp section, just dispatched on a background thread or coroutine.
E. Jakarta EE `@Schedule` — periodic batch log
E. Jakarta EE `@Schedule` — periodic batch log
```java
@Stateless
public class BatchLogger {
@Inject FreshJots fj;
@Schedule(hour = "*", minute = "0", persistent = false)
public void hourly() throws Exception {
fj.append("batch-log", "tick — " + LocalDateTime.now());
}
}
```
Works in any Jakarta EE 10+ app server (WildFly, Payara, Open Liberty). `persistent = false` because if the app server is down, you've already lost the heartbeat — no point queueing missed firings; the next run is in an hour.
F. Maven Surefire — append on test-suite finish
```xml
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-DFRESHJOTS_TOKEN=${env.FRESHJOTS_TOKEN}</argLine>
</configuration>
</plugin>
```
Then a `@AfterAll` hook in your top-level test class:
```java
@AfterAll
static void reportToFreshJots() throws Exception {
new FreshJots().append("ci-test-runs", "build " + System.getenv("BUILD_NUMBER") + " — green");
}
```
CI builds drop a one-line entry in `ci-test-runs` after each successful run. Pair with `append_deadline_hours` to alert when builds stop happening — usually a sign the CI itself is broken.
6. Loading the token
6. Loading the token
Four good options, ordered local → production:
- **Shell profile** (`~/.bashrc`, `~/.zshrc`) — see Get & set your Fresh Jots API token. Best for local development.
- **Spring profile / `application.properties`** — `freshjots.token=${FRESHJOTS_TOKEN}` reads from env at boot; `application-prod.properties` for prod overrides. Never inline the token value in the properties file.
- **Container env** — Kubernetes `Secret` mounted as env var, ECS task definition `secrets:`, Fly.io `fly secrets set`, Cloud Run revision env. Always for production.
- **Vault / Secrets Manager** — for orgs already running HashiCorp Vault or AWS Secrets Manager, mount the secret at boot via Spring Cloud Vault or the AWS SDK and inject into the bean.
Don't commit the token. Don't commit `application-local.properties` if it inlines secrets. `git check-ignore -v config.properties` should print a match.
7. Going further
7. Going further
- **`HttpClient` reference (JDK 21):** [docs.oracle.com/en/java/javase/21/docs/api/java.net.http](https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/http/HttpClient.html). The two pages worth bookmarking: `HttpRequest.BodyPublishers` and `HttpResponse.BodyHandlers`.
- **Other languages — same pattern, different HTTP client.** Hub: Write a note from any project.
- **The CLI** — for one-off shell-from-Java scenarios via `ProcessBuilder`. See Notes from your terminal.
- **Dead-man alerts** — pair a per-batch note with `append_deadline_hours` and Fresh Jots emails you when the batch goes silent. See Everything you can do here.
One method, one Authorization header, one note in your account — pasted into a Spring service today, alerting you about silent cron jobs tomorrow.