Skip to content

Part 2: Add a Cloud Run Service

In Part 1 you deployed a Cloud Storage Bucket. Now you put an HTTP service on Cloud Run that reads and writes objects in it.

A Cloud Run service is a class with an infrastructure definition and a runtime implementation. Create src/api.ts:

src/api.ts
import * as GCP from "@alchemy.run/gcp";
import * as Effect from "effect/Effect";
export default class Api extends GCP.Function<Api>()(
"Api",
{ main: import.meta.url },
Effect.gen(function* () {
return {};
}),
) {}

GCP.Function is a Cloud Run service. main: import.meta.url makes this file the entrypoint: Alchemy bundles it into a container image, pushes it to Artifact Registry, and deploys it.

Return a fetch handler to answer requests:

src/api.ts
import * as GCP from "@alchemy.run/gcp";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
export default class Api extends GCP.Function<Api>()(
"Api",
{ main: import.meta.url },
Effect.gen(function* () {
return {};
return {
fetch: Effect.succeed(HttpServerResponse.text("Hello from Cloud Run!")),
};
}),
) {}

Cloud Run asks every caller for a Google identity token by default. Turn that off so anyone can call the service:

export default class Api extends GCP.Function<Api>()(
"Api",
{ main: import.meta.url },
{ main: import.meta.url, invokerIamDisabled: true },
Effect.gen(function* () {
return {
fetch: Effect.succeed(HttpServerResponse.text("Hello from Cloud Run!")),
};
}),
) {}

Declare the Bucket next to the code that uses it:

Effect.gen(function* () {
const bucket = yield* GCP.Storage.Bucket("Bucket", {});
return {
fetch: Effect.succeed(HttpServerResponse.text("Hello from Cloud Run!")),
};
}),

The logical id is still Bucket, so this is the same Bucket you deployed in Part 1. Moving a declaration doesn’t recreate it.

Ask for read and write access to the Bucket:

Effect.gen(function* () {
const bucket = yield* GCP.Storage.Bucket("Bucket", {});
const files = yield* GCP.Storage.ReadWriteBucket(bucket);
return {
fetch: Effect.succeed(HttpServerResponse.text("Hello from Cloud Run!")),
};
}),

ReadWriteBucket gives you a client with get, put, list, and delete. On deploy it grants the service roles/storage.objectUser on this Bucket and nothing else. Use ReadBucket or WriteBucket when you need less.

Use the URL path as the object name and store the request body:

src/api.ts
import * as GCP from "@alchemy.run/gcp";
import * as Effect from "effect/Effect";
import { HttpServerRequest } from "effect/http/HttpServerRequest";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
export default class Api extends GCP.Function<Api>()(
"Api",
{ main: import.meta.url, invokerIamDisabled: true },
Effect.gen(function* () {
const bucket = yield* GCP.Storage.Bucket("Bucket", {});
const files = yield* GCP.Storage.ReadWriteBucket(bucket);
return {
fetch: Effect.succeed(HttpServerResponse.text("Hello from Cloud Run!")),
fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
const key = new URL(request.originalUrl, "http://localhost").pathname.slice(1);
if (request.method === "PUT") {
const body = yield* request.text;
yield* files.put(key, body, { contentType: "text/plain" });
return HttpServerResponse.empty({ status: 204 });
}
return HttpServerResponse.text("Method not allowed", { status: 405 });
}),
};
}),
) {}

put takes the object name, a string or bytes, and options like contentType.

Add a GET branch:

if (request.method === "PUT") {
const body = yield* request.text;
yield* files.put(key, body, { contentType: "text/plain" });
return HttpServerResponse.empty({ status: 204 });
}
if (request.method === "GET") {
const object = yield* files.get(key);
if (object === undefined) {
return HttpServerResponse.text("Not found", { status: 404 });
}
return HttpServerResponse.uint8Array(object.body, {
contentType: object.contentType ?? "application/octet-stream",
});
}
return HttpServerResponse.text("Method not allowed", { status: 405 });

get returns undefined for a missing object, so a 404 is a plain if.

Apply Effect.orDie once at the end of fetch:

fetch: Effect.gen(function* () {
// ... PUT and GET branches ...
return HttpServerResponse.text("Method not allowed", { status: 405 });
}),
}).pipe(Effect.orDie),

Any failure you didn’t handle becomes a 500.

Bindings declare what the service needs. Provide the layer that implements the Bucket client at the end of the service:

export default class Api extends GCP.Function<Api>()(
"Api",
{ main: import.meta.url, invokerIamDisabled: true },
Effect.gen(function* () {
// ... bucket, binding, fetch ...
}),
}).pipe(Effect.provide(GCP.Storage.ReadWriteBucketHttp)),
) {}

Every binding has a matching Http layer: ReadWriteBucket → ReadWriteBucketHttp. Forget one and the type error names it.

Yield Api in the Stack and return its URL:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as GCP from "@alchemy.run/gcp";
import * as Effect from "effect/Effect";
import Api from "./src/api.ts";
export default Alchemy.Stack(
"MyApp",
{
providers: GCP.providers(),
state: GCP.state(),
},
Effect.gen(function* () {
const bucket = yield* GCP.Storage.Bucket("Bucket", {});
const api = yield* Api;
return {
bucketName: bucket.bucketName,
url: api.uri,
};
}),
);

The Bucket is still part of the Stack. Api declares it.

Terminal window
pnpm alchemy deploy
Plan: 1 to create

+ Api (GCP.Run.Service)
• Bucket (GCP.Storage.Bucket)

Proceed?
◉ Yes ○ No
• Bucket (GCP.Storage.Bucket) no change
✓ Api (GCP.Run.Service) created
{
  url: "https://myapp-live-sam-api-a1b2c3-uc.a.run.app",
}

Alchemy builds the image, creates a service account for the service, grants it access to the Bucket, and deploys the revision. The first deploy takes a couple of minutes. Later deploys only rebuild when your code changes.

Terminal window
curl -X PUT "$URL/hello.txt" -d 'Hello, world!'
curl "$URL/hello.txt"
# → Hello, world!

You now have:

  • A Cloud Run service with GET and PUT routes on a public URL
  • A Bucket bound to the service, with IAM granted from the binding
  • Stack outputs showing the service URL

In Part 3, you write tests that deploy the Stack and call the service over HTTP.