@jamone19/nuxt-atomic
v0.1.0
Published
Nuxt 4 module for atomic (saga-like) multi-step transactions with automatic server routes, rollbacks, and logging.
Readme
@jamone19/nuxt-atomic
Atomic (saga-like) multi-step transactions for Nuxt 4 with automatic server routes, typed hydrators, rollback orchestration, and structured logging.
Atomic brings a declarative transactional workflow to Nuxt, allowing your application to daisy chain API calls into Atomic Transactions
Basics:
register steps, run, rollback safely.
- Register a transaction → define ordered steps (
execute,rollback,hydrate). - Call
POST /api/atomic/<TransactionName>→ steps run sequentially; on failure, prior steps roll back in reverse. - Pass data across steps: outputs accumulate (
chainAcc) and GET window results join the next PUT input. - Works with any package manager:
yarn add @jamone19/nuxt-atomic,pnpm add @jamone19/nuxt-atomic, ornpm i @jamone19/nuxt-atomic.
Install
pnpm add @jamone19/nuxt-atomic
# or: yarn add @jamone19/nuxt-atomic
# or: npm i @jamone19/nuxt-atomicQuickstart
nuxt.config.ts
export default defineNuxtConfig({
modules: ['@jamone19/nuxt-atomic'],
atomic: {
baseRoute: '/api/atomic', // optional (default '/api/atomic')
logDir: '.nuxt-atomic/logs', // optional
// registryPath: '~/atomic/registry', // reserved/optional (current build inlines transactions)
transactions: {
CreateUser: {
steps: [
{
key: 'CreateDBUser',
execute: { method: 'POST', url: 'http://localhost:3001/mock/users' },
rollback: { method: 'DELETE', url: 'http://localhost:3001/mock/users/:id' },
hydrate: { module: '~/atomic/steps/userCreate', export: 'hydrateCreate' }
},
{
key: 'GrantWelcomeCredits',
execute: { method: 'POST', url: 'http://localhost:3001/mock/credits/grant' },
rollback: { method: 'POST', url: 'http://localhost:3001/mock/credits/revoke' },
hydrate: { module: '~/atomic/steps/grantCredits', export: 'hydrate' }
}
]
},
// Example of a chained flow: PUT → GET → GET → PUT
CreateUserEverywhere: {
steps: [
{
key: 'CreateInServiceA',
execute: { method: 'POST', url: 'http://localhost:3001/mock/users' },
rollback: { method: 'DELETE', url: 'http://localhost:3001/mock/users/:id' },
hydrate: { module: '~/atomic/steps/createA', export: 'hydrate' }, // PUT
mode: 'put'
},
{
key: 'FetchAProfile',
execute: { method: 'GET', url: 'http://localhost:3001/mock/users/:id/profile' },
rollback: { method: 'GET', url: 'http://localhost:3001/mock/nop' },
hydrate: { module: '~/atomic/steps/fetchAProfile', export: 'hydrate' }, // GET
mode: 'get'
},
{
key: 'FetchARecommendations',
execute: { method: 'GET', url: 'http://localhost:3001/mock/users/:id/recs' },
rollback: { method: 'GET', url: 'http://localhost:3001/mock/nop' },
hydrate: { module: '~/atomic/steps/fetchARecs', export: 'hydrate' }, // GET
mode: 'get'
},
{
key: 'CreateInServiceB',
execute: { method: 'POST', url: 'http://localhost:3001/mock/usersB' },
rollback: { method: 'DELETE', url: 'http://localhost:3001/mock/usersB/:id' },
hydrate: { module: '~/atomic/steps/createB', export: 'hydrate' }, // PUT uses original + GET window
mode: 'put'
}
]
}
}
}
})Step hydrator example ~/atomic/steps/userCreate.ts
import { createHydrator } from '@jamone19/nuxt-atomic/runtime'
type In = { name: string; email: string; credits?: number } & Record<string, any>
type Out = { name: string; email: string }
export const hydrateCreate = createHydrator<In, Out>((input, ctx) => {
return { name: input.name, email: input.email }
})Run
pnpm -w build && pnpm demo
# curl
curl -sS -X POST http://localhost:3000/api/atomic/CreateUser -H 'content-type: application/json' -d '{"name":"Ada","email":"[email protected]","credits":50}' | jqE2E tests
pnpm -C examples/mock-server i
pnpm -C examples/nuxt-app i
pnpm test:e2eThe suite spawns the mock server and the Nuxt dev server, then calls the atomic routes. Coverage (V8) is emitted to ./coverage and uploaded by CI as an artifact.
Roadmap
We’re looking for collaborators & sponsors to help shape the roadmap.
Planned and proposed items:
- Retries & backoff policies (per-step and per-rollback; jitter, exponential)
- Parallel / fan‑out steps with fan‑in joins and data merge policies
- Idempotency keys and deduplication for execute & rollback
- Persistence & recovery (transaction store + resume/retry after crash/redeploy)
- Observability (OpenTelemetry traces, structured events, step timelines)
- Circuit breakers & timeouts (fail‑fast, trip conditions, half‑open recovery)
- Conditional branches & guards in the transaction DSL
- Schema validation for inputs/outputs (Zod) + typed codegen for hydrators
- DevTools panel to inspect live transactions, replay, and dry‑run
- CLI (inspect queue, force resume, retry/abort, dump timelines)
- Pluggable sinks for logs/metrics (stdout, file, HTTP, S3, Grafana/Loki)
- Provider adapters (Stripe, Supabase, Clerk, SendGrid, etc.)
- Exactly‑once patterns (outbox/inbox for webhook-driven steps)
- Cron / scheduler for deferred retries and time‑based compensations
- Versioning & migrations (evolve transaction definitions safely)
- Secrets integration (Nuxt runtimeConfig, per‑step headers, signing)
- RBAC & policy hooks for who/what can execute specific transactions
Have an idea you need? Open an issue — we’re happy to prioritize features that unblock real workloads.
Sponsorship ♥
If this project helps you ship faster, please consider sponsoring development.
- Ko‑fi: https://ko-fi.com/jamone19
Thank you — your support helps us maintain the module, write docs, and build new features.
End-to-End Demo (with mock server)
- Start mock server
pnpm -w build
pnpm -C examples/mock-server i
pnpm -C examples/mock-server start- Start example app
pnpm -C examples/nuxt-app i
pnpm -C examples/nuxt-app dev- Call the transaction
curl -X POST http://localhost:3000/api/atomic/CreateUser -H 'content-type: application/json' -d '{"name":"Ada","email":"[email protected]","credits":50}'See detailed walkthrough in docs/mock-server.md.
Run in a podman container
podman run -it --rm -p 3200-3205:3000-3005 -v $(pwd):/app:z -v /app/node_modules node:22-alpine /bin/sh
apk add --update nodejs bash vim yarn git npm pnpm curl jq
bash
cd /appConcurrent demo runner
pnpm i
pnpm -w build
pnpm demo
# runs mock server (3001) and example Nuxt app (3000) together
# quick test
curl -X GET http://localhost:3001/health -H 'content-type: application/json' -d '{"name":"Ada","email":"[email protected]","credits":50}'
output:
{"ok":true}Test demo app
Create once and capture Service A ID
RES=$(curl -sS -X POST http://localhost:3000/api/atomic/CreateUserEverywhere \
-H 'content-type: application/json' \
-d '{"name":"A","email":"[email protected]"}')
A_ID=$(echo "$RES" | jq -r '.results[] | select(.step=="CreateInServiceA") | .result.id')
echo "Service A ID: $A_ID"Hit mock endpoints directly
curl -sS "http://localhost:3001/mock/users/$A_ID/profile" | jq
curl -sS "http://localhost:3001/mock/users/$A_ID/recs" | jqCreate a User across multiple services
curl -sS -X POST "http://localhost:3000/api/atomic/CreateUserEverywhere" \
-H "content-type: application/json" \
-d '{"name":"Alicia","email":"[email protected]"}' | jqE2E tests (Vitest)
Requires you to install deps in both example folders.
pnpm -C examples/mock-server i
pnpm -C examples/nuxt-app i
pnpm test:e2eThe tests spin up both processes, verify CreateUser succeeds, and CreateUserFail triggers a rollback with a proper report.
Quick health/handler checks
Unknown transaction (proves handler is mounted):
curl -sS -X POST "http://localhost:3000/api/atomic/Nope" -H "content-type: application/json" -d '{}' | jqTail the atomic log (shows step execution & rollbacks):
tail -f examples/nuxt-app/.nuxt-atomic/logs/atomic.logStep modes (data passing)
Optionally set mode per step:
mode?: 'put' | 'get' | 'noop' // defaults: GET -> 'get', otherwise 'put'- Results from all steps accumulate into
chainAcc. - Results from get steps accumulate into a window that resets after each put.
- Hydrators receive a composed payload:
- For get steps:
{ ...original, ...chainAcc } - For put steps:
{ ...original, ...chainAcc, ...getWindowSinceLastPut }This lets you create flows like: PUT → GET → GET → GET → PUT, where the second PUT can use original input plus the outputs of the three GETs.
- For get steps:
Typed hydrators
Import the helper and type your input/output precisely:
import { createHydrator } from '@jamone19/nuxt-atomic/runtime'
type In = { name: string; email: string } & Record<string, any>
type Out = { name: string; email: string }
export const hydrate = createHydrator<In, Out>((input, ctx) => {
return { name: input.name, email: input.email }
})Hydrators receive a composed input plus a context with chainAcc, windowAcc, etc.
PNPM workspace
The repo includes a workspace so one install wires everything:
pnpm ipnpm-workspace.yaml includes examples/*, so example apps share the root lockfile and hoisted deps.
Example: PUT → GET → GET → PUT
- Step 1 (PUT)
CreateInServiceA: creates a user in Service A and returns{ id, ... }. - Step 2 (GET)
FetchAProfile: fetches a profile for:id. Result is added to the GET-window. - Step 3 (GET)
FetchARecommendations: fetches recommendations for:id. Also added to the GET-window. - Step 4 (PUT)
CreateInServiceB: receivesoriginal + chainAcc + windowAcc. This includes theidfrom Step 1 and the profile/recommendations from Steps 2–3. After this step, the GET-window is cleared.
Call it:
curl -X POST http://localhost:3000/api/atomic/CreateUserEverywhere -H 'content-type: application/json' -d '{"name":"Alicia","email":"[email protected]"}'Coverage
E2E tests run in a separate process model (mock server + Nuxt app), so coverage reflects code executed directly in the test process (e.g., helpers). We still publish reports for visibility.
Local:
pnpm test:e2e # open coverage/index.htmlCI uploads
coverage/as an artifact named coverage-report.
Programmatic Nuxt boot in tests
The E2E suite boots Nuxt inside the Vitest process using loadNuxt + buildNuxt, and starts the mock server in-process as well. This improves V8 coverage for the server handler code paths.
Branding
SVGs live in branding/ (icon, horizontal, stacked). Drop the horizontal lockup at the top of this README:
<p align="center">
<img src="./branding/nuxt-atomic-logo-horizontal.svg" width="640" alt="Nuxt Atomic" />
</p>License
MIT
