Two ways to call a raw API from Pulumi
Two ways to consume raw APIs from Pulumi when no first-class provider exists: a Dynamic Provider (full lifecycle) and @pulumi/command (fire-and-forget shells).
This example lives in the pulumi/examples repository. Check out just this directory to use it:
git clone --filter=blob:none --sparse https://github.com/pulumi/examples pulumi-examplesgit -C pulumi-examples sparse-checkout set ts-raw-api-providerscd pulumi-examples/ts-raw-api-providersThere are two paths when the API you want to use has no Pulumi provider yet, and you don’t want to wait around for one. This example does both, against real public endpoints, so you can see how they actually behave.
pulumi.dynamic.Resource— write a custom resource withcreate,read,update,delete, anddiff. Pulumi stores it in state, diffs it like any other resource, and calls the right method onup/refresh/destroy. This is what you want when the thing has an identity and a lifecycle.@pulumi/command— shell out a command on create, optionally another on delete. There’s no state of the remote object, just the command itself. Fine for one-shot effects (a curl call, a CLI invocation).
Both patterns hit real public HTTP endpoints. No local files, no fakes.
| Pattern | Endpoint | Verbs used |
|---|---|---|
Dynamic provider (HttpbinEcho) | https://httpbin.org/anything | POST, GET, PUT, DELETE |
command.local.Command (zen) | https://api.github.com/zen | GET via curl |
Files#
| File | What it does |
|---|---|
httpbinEcho.ts | The dynamic provider. HttpbinEcho class with the CRUD methods, each backed by fetch(). |
index.ts | Uses HttpbinEcho (pattern 1) and command.local.Command (pattern 2). |
Run it#
npm installpulumi stack init lumitorch/devpulumi up --yespulumi stack output # echoUrl, echoEtag, echoLastMethod=POST, echoTraceId, echoedBody, githubZen
# Update path: change an input, watch it call update() (PUT) instead of create() (POST):pulumi config set message "edited via pulumi up"pulumi up --yespulumi stack output # echoLastMethod is now PUT, etag and traceId changed, echoedBody.message updated
# Delete path:pulumi destroy --yes # calls delete() → DELETE https://httpbin.org/anythingHow the dynamic provider works#
The five methods on provider get serialized by Pulumi, closures and all,
and run in a separate Node process. Two things to know:
- Method bodies have to be self-contained. We
await import("node:crypto")inside each method (and use Node 18+‘s built-infetch) so nothing top-level is captured by the closure serializer. - The
idreturned fromcreateis the resource’s identity for every laterread/update/deletecall. Here we use httpbin’sX-Amzn-Trace-Idas a stand-in. diffdecides between an in-place update (changes: true) and a replace (replaces: ["someProp"]).
Pointing this at a real REST API#
Swap the four fetch calls in httpbinEcho.ts for calls against your real
service. The CRUD shape stays the same:
create → POST /resource → returns body with idread → GET /resource/{id} → returns bodyupdate → PUT /resource/{id} → returns bodydelete → DELETE /resource/{id} → 204Which one do I want?#
| Need | Use |
|---|---|
| Real lifecycle, diffs, refresh, outputs tracked in state | Dynamic provider |
| Fire a curl/CLI on create, optionally clean up on delete | @pulumi/command |
| Reuse across projects or languages | Build a real provider via the Terraform bridge or provider-boilerplate |