Store and validate a file in memory
Write, read and delete a typed asset without a bucket account or credentials.
An asset declares the content types and size your application accepts. A bucket Layer supplies the storage implementation. Start with memory to exercise the same asset operations you will use with persistent storage.
Try a write and an oversized overwrite
Each run starts fresh. Nothing is sent to a storage service.
Result
Choose inputs, then run the example.A successful run writes your text, reads it back and deletes the object. The final existsAfterDelete is false.
Choose Try 66 bytes, then run again. AssetTooLarge is returned and the read still contains Original note. A failed overwrite does not replace the existing object. The limit counts bytes: é occupies two bytes in UTF-8, so 33 characters exceed the 64-byte limit.
Each run creates a new memory bucket. The demo seeds an object first so you can see what happens to an existing value when a write is refused.
Run the same code locally
Use Node.js 24 and install the package:
pnpm add @kutur-kit/assets@alpha effectSave this as storage-demo.ts, then run node storage-demo.ts. Change the invocation to storageDemo("é".repeat(33)) to exercise the size limit.
import { Asset, AssetBucket } from "@kutur-kit/assets";
import * as Memory from "@kutur-kit/assets/memory";
import { Effect, Option, Stream } from "effect";
class Notes extends AssetBucket.Service<Notes>()("demo/Notes") {}
const NoteFile = Asset.make("note", {
bucket: Notes,
contentTypes: ["text/plain"],
maxSize: 64,
});
const key = Asset.Key.make("welcome.txt");
const write = (text: string) =>
NoteFile.put(key, Stream.succeed(new TextEncoder().encode(text)), { contentType: "text/plain" });
export const storageDemo = (text: string) =>
Effect.gen(function* () {
yield* write("Original note");
const result = yield* write(text).pipe(
Effect.as("Saved"),
Effect.catchTag("AssetTooLarge", () => Effect.succeed("AssetTooLarge")),
);
const stored = yield* NoteFile.get(key);
const content = Option.isSome(stored)
? yield* Stream.mkString(Stream.decodeText(stored.value.body))
: null;
yield* NoteFile.delete(key);
const afterDelete = yield* NoteFile.get(key);
return {
result,
attemptedBytes: new TextEncoder().encode(text).length,
storedKey: yield* NoteFile.storedKey(key),
content,
existsAfterDelete: Option.isSome(afterDelete),
};
}).pipe(Effect.provide(Memory.layer(Notes)));
Effect.runPromise(storageDemo("Hello from Kit")).then(console.log);Define the contract once
AssetBucket.Service names a storage service. Asset.make defines the asset's ID, bucket, content types and maximum size. The stored key is prefixed with the asset ID, so welcome.txt becomes note/welcome.txt.
put consumes a stream of bytes. get returns an Option: a missing object is None, not an exception. The example decodes the returned stream into text because this asset accepts text/plain.
This example uses a known key and content type. For request input, decode keys with NoteFile.Key and content types with NoteFile.ContentType. These schemas reject invalid values before you use them. Declaring a content type does not inspect the contents of a file.
Keep the Layer alive for the right scope
Memory.layer(Notes) owns its map for the lifetime of the Layer. Provide it around the whole program when several operations should share objects. Creating and providing a fresh memory Layer for every request does not preserve data across requests.
For tests, a fresh Layer per case keeps the stores isolated. For an application, use filesystem, S3 or R2 storage when objects must survive a process restart.
What memory does not prove
This demo covers streamed writes, asset validation, reads and deletes. It does not exercise network failures, cloud credentials, CORS or signed URL delivery. A memory bucket alone does not expose an HTTP upload or download endpoint.
Before using direct uploads, follow the signed upload and download guidance and test the chosen signing and delivery adapters with the storage service you will deploy.