migrate-sdk
Cookbook

API to API

Read authors from an API, map them with schemas, and write repeatable destination requests.

This recipe implements a Source over HTTP, calls a destination API from a Process Pipeline, and preserves a Tracking Record for recovery. It applies the same primitives as CSV to JSON files.

The sample payload contains author records. Their fields illustrate the mapping; identity, versioning, tracking, and rollback follow the SDK contracts described in Learn.

The API contract

The endpoints are illustrative. Point the environment variables at APIs you own that implement this contract, or adapt the small transport helper to your provider.

RequestRequired behavior
GET SOURCE_URLReturn the complete { authors: [...] } document shown below.
PUT DESTINATION_URL/:idCreate or replace that exact ID; repeating it must converge on the same record.
DELETE DESTINATION_URL/:idRemove that record; 404 means it is already absent.
Source response
{
  "authors": [
    { "id": "author-1040", "first_name": "Ada", "last_name": "Morgan" },
    { "id": "author-1041", "first_name": "Alex", "last_name": "Chen" }
  ]
}

The destination receives { "id": "imported-author-1040", "name": "Ada Morgan" }. Reserve the imported- namespace for this migration: rollback deletes records in it. This example does not restore preexisting records that a PUT replaced.

Define the source and migration

Install the dependencies from Getting started. Save both files below in the same directory. The first file contains the complete migration config; the second implements HTTP requests, authentication, timeout, and error wrapping.

api-to-api.ts
import { ,  } from "effect";
import {
  type ,
  ,
  ,
  type ,
  type ,
  ,
  ,
} from "migrate-sdk";
import {  } from "migrate-sdk/cli";
import {
  ,
  ,
  ,
} from "migrate-sdk/sources/document";
import {  } from "migrate-sdk/stores/file";
import { ,  } from "./api-http";

export const  = .({
  : .,
  : .,
  : .,
});
export const  = .({
  : .(),
});
export const  = .({
  : .,
  : .,
});

// Point these at APIs implementing the contract described in the recipe.
export const  = {
  : .. ?? "https://legacy.example.test/authors",
  : ..,
};
export const  = {
  : .. ?? "https://directory.example.test/authors",
  : ..,
};

export const  = .({
  : .({
    : .,
    : () =>
      ().(.(() => ({  }))),
  }),
  : .("authors-api", ),
  : { : () => . },
  : {
    : "author-id@v1",
    : .("authorId", .),
    : ({  }) => .,
  },
  : { : "content-hash" },
  : { : "scan" },
});

// This namespace must belong exclusively to this migration.
export const  = (: string) => `imported-${}`;
export const  = (: typeof .) =>
  .()({
    : (.),
    : `${.} ${.}`,
  });

const  = .({
  : "author-api@v1",
  : .({ : . }),
});

type  = <
  typeof ,
   | .,
  typeof 
>;
const :  = .("processApiAuthor")(
  function* () {
    const  = yield* (..);
    yield* (, ., "PUT", );
    yield* .({ : . });
  }
);

const : <
  typeof ,
   | .
> = .("rollbackApiAuthor")(function* () {
  // A timed-out PUT may have succeeded before tracking was saved.
  const  = yield* .(.)(
    ..
  );
  const  = .?. ?? ();
  yield* (, , "DELETE");
});

export const  = .({
  : "api-authors",
  : ,
  : ,
  : ,
  : ,
  : .({ : "./.migrate-api" }),
});

export default ({
  : .({ : [] }),
});

DocumentSource selects document.authors, validates their shape, and hashes each selected author for change detection. The fetcher supplies one complete document, so there is no pagination cursor to implement. Schema.Null declares that fetcher contract.

Document selection wraps the selected value in { item }. The process therefore reads sourceItem.item.item: the outer .item is the source payload, and the inner .item is the selected author. The Sources page explains adapter payloads.

Lookup uses a scan. A targeted retry can fetch and inspect the whole document to find one ID. Use a direct lookup or a paginated custom source when the provider and dataset justify it.

Configure and run

Set SOURCE_URL and DESTINATION_URL to the collection URLs. Set SOURCE_TOKEN and DESTINATION_TOKEN if the APIs use bearer authentication. Keep those credentials in your shell or deployment environment.

pnpm exec migrate run api-authors --config ./api-to-api.ts --plan
pnpm exec migrate run api-authors --config ./api-to-api.ts
pnpm exec migrate status api-authors --config ./api-to-api.ts

The plan checks the operation; it does not prove that the API credentials, payload contract, or destination permissions work. Start against a test environment whose imported records you can delete.

The file store saves progress under .migrate-api. Keep that directory between invocations, and keep this config pointed at the same destination. Reusing progress from one environment against another can make the SDK treat records as already migrated when the new destination is empty.

Repeat and recover

Rerun normally after changing an author's source fields. Its content hash changes and the same destination ID receives the new payload. An unchanged migrated author does not need another write.

For an item that failed, fix the endpoint, credentials, or mapping and run:

pnpm exec migrate run api-authors --config ./api-to-api.ts --failed

A timeout does not prove the destination rejected a write. The predictable PUT path allows a repeat to converge on the same record. Rollback first uses the saved tracking ID; for a failed item without a success record it derives the migration-owned ID from the saved source identity. It never needs the source API to look up that author again.

pnpm exec migrate rollback api-authors --config ./api-to-api.ts --plan
pnpm exec migrate rollback api-authors --config ./api-to-api.ts

If a process crashes before any item state is saved, that item may not yet be available to rollback. Resume or reconcile the destination first. Neither an HTTP response nor a tracking call makes the API and store one transaction.

Add resilience deliberately

The transport helper has a ten-second timeout per request and makes no automatic retries. To add bounded backoff, preserve status and transient-failure information at the HTTP boundary and use the policy in Retries and backoff. Do not retry every generic DestinationError: authentication failures and rejected data need different treatment from a 503.

For large datasets, replace the complete-document fetch with a paginated source. Its cursor must be stable enough to resume, and its discovery mode must allow updates to existing authors to be found. The next recipe first adds another source: Combine two APIs.

On this page