"How did you test that endpoint?" — "I copied the curl from Postman." That sentence is why JQuickCurl exists. When a request works in Postman or the browser DevTools, the raw curl fragment is already the ground truth. In this post you'll learn a repeatable recipe:
- Export curl from Postman or copy it from Chrome DevTools.
- Sanitize it (remove volatile headers and secrets).
- Paste it into
@JCurlCommand. - Turn the moving parts into
${...}variables. - Run the exact same request from Java.
Step 1: Get the curl Snippet
From Postman: open your saved request → the </> Code button in the top-right → choose cURL → copy.
From Chrome DevTools: open the Network tab → right-click the request you care about → Copy → Copy as cURL (bash).
A typical browser copy looks like this (notice the noise):
curl 'https://api.example.com/orders/1024' \
-H 'accept: application/json, text/plain, */*' \
-H 'accept-language: en-US,en;q=0.9' \
-H 'cookie: _ga=GA1.2.abc; session=7f3a9c1e...' \
-H 'sec-ch-ua: "Chromium";v="120", "Google Chrome";v="120"' \
-H 'sec-fetch-site: same-origin' \
-H 'user-agent: Mozilla/5.0 ...'
Step 2: Sanitize Before You Paste
Rule number one of reusing curl in server code: the browser context is not your server context. Remove or externalize:
-
cookie/authorizationvalues (inject them at runtime instead — see Post 12 on interceptors). - Browser fingerprint headers (
sec-ch-ua,sec-fetch-*,user-agent) unless the API genuinely validates them. - Your
_gatracking cookies — you do not want analytics cookies in server logs.
Keep the semantic core: method, URL, Content-Type, real payload, and any header the API contract actually requires.
Step 3: Paste into an Annotation — and Mind the Escaping
Collapse the multi-line shell command onto one line and put it inside @JCurlCommand:
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
public interface OrderApi {
@JCurlCommand("curl -X GET 'https://api.example.com/orders/1024' "
+ "-H 'accept: application/json' "
+ "-H 'X-Client: jquick-curl'")
String getOrder(JQuickCurlReq request);
}
Java-string escaping rules are the only friction you'll meet:
- Postman/browser use double quotes in headers (
-H "Accept: ..."); inside an annotation they must be escaped or switched to single quotes. I recommend single quotes — they read cleanly and match shell-style curl. - If your JSON body itself contains single quotes, escape them with
\'inside the single-quoted value, or keep double quotes and escape them as\".
Tip: concatenating adjacent string literals (
"..." + "...") keeps long commands readable and still satisfies Java's requirement that annotation values be compile-time constants.
Step 4: Make the Moving Parts Dynamic
A hard-coded order ID dies on the first real request. Replace volatile values with ${name} placeholders and supply them through JQuickCurlReq:
public interface OrderApi {
@JCurlCommand("curl -X GET 'https://api.example.com/orders/${orderId}' "
+ "-H 'accept: application/json'")
String getOrder(JQuickCurlReq request);
}
JQuickCurlReq request = new JQuickCurlReq();
request.put("orderId", 1024);
String json = JCurlInvoker.createProxy(OrderApi.class).getOrder(request);
System.out.println(json);
JQuickCurlReq is literally a HashMap<String, Object>, so put/get behave exactly as you'd expect. Whatever key you place there can be referenced as ${key} anywhere in the command — URL, header value, -u credentials, -d body.
Step 5: Full "Pasted from Postman" Demo
Here's a complete runnable example using the public httpbin echo service. Copy this curl from your own Postman and you can verify the flow end-to-end:
import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;
public interface PostmanApi {
// The exact command you exported, after removing volatile headers.
@JCurlCommand("curl -X POST 'https://httpbin.org/post' "
+ "-H 'Content-Type: application/json' "
+ "-d '{\"orderId\":${orderId},\"customer\":\"${customer}\"}'")
String postOrder(JQuickCurlReq request);
}
class Demo {
public static void main(String[] args) {
PostmanApi api = JCurlInvoker.createProxy(PostmanApi.class);
JQuickCurlReq req = new JQuickCurlReq();
req.put("orderId", 555);
req.put("customer", "Hashnode reader");
System.out.println(api.postOrder(req));
// httpbin echoes back the "data" field containing your substituted JSON.
}
}
What If the Pasted Command Fails to Parse?
JQuickCurl's parser targets the curl options its test suite covers. Before debugging your own code, double-check that the exported snippet only uses these option families: -X/--request, -H/--header, -d/--data(-ascii|-binary|-raw|-urlencode), -u/--user, -L/--location/--max-redirs, -F/--form, -o/--output, -x/--proxy/--socks5-hostname, plus --http2, -k/--insecure, -v/--verbose, -s/--silent. Exotic flags (--compressed, --resolve, --path-as-is, …) are not part of the supported set — strip them from the fragment you paste.
Summary
The workflow that used to take "translate request → write builder code → pray" now takes seconds: copy curl → sanitize → paste → parameterize. JQuickCurl closes the gap between the tool where you debug HTTP (Postman/browser) and the code where you ship it. All examples in this series run against dromara/jquick-curl.
Next up, in Post 5, we take a deep dive into the @JCurlCommand annotation and its variable-substitution grammar — the feature that turns static curl strings into dynamic production requests.
Top comments (2)
Step 2 is the one people skip, and the reason JQuickCurl-type tools get a bad reputation is exactly that: someone pastes a DevTools copy verbatim, the committed annotation now carries a session cookie, and the first rotation breaks prod.
We hit the same wall with a different stack — our collectors replay browser-copied requests through a script rather than a typed client. The difference that made it maintainable: strip at parse time with an allowlist instead of a denylist. Your post lists what to remove (cookie, sec-ch-ua, _ga); we removed everything except method, path, content-type and a declared custom-header set, so a new fingerprint header added by Chrome in a month is dropped by default rather than silently forwarded.
Does the library expose any hook for that — an option to drop volatile headers and warn, before the annotation string is parsed? If yes, worth adding to the post as the default advice; if no, that's the first thing I'd want before reusing this beyond tutorials.
Our early versions worked exactly like that. But we've since added support for putting sensitive cookies or auth credentials into variable environments, and made the parser more compatible with the curl fragments people actually copy — so in most cases you don't need to sanitize them manually anymore, which makes development much faster