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.
Create the service file
Section titled “Create the service file”A Cloud Run service is a class with an infrastructure definition and a
runtime implementation. Create 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.
Serve HTTP with fetch
Section titled “Serve HTTP with fetch”Return a fetch handler to answer requests:
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!")), }; }),) {}Make it public
Section titled “Make it public”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!")), }; }),) {}Move the Bucket into the service
Section titled “Move the Bucket into the service”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.
Bind the Bucket
Section titled “Bind the Bucket”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.
Write objects with PUT /:key
Section titled “Write objects with PUT /:key”Use the URL path as the object name and store the request body:
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.
Read objects with GET /:key
Section titled “Read objects with GET /:key”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.
Turn unhandled errors into 500s
Section titled “Turn unhandled errors into 500s”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.
Provide the client
Section titled “Provide the client”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.
Add the service to the Stack
Section titled “Add the service to the Stack”Yield Api in the Stack and return its URL:
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.
Deploy
Section titled “Deploy”pnpm alchemy deploybun alchemy deploynpx alchemy deployyarn alchemy deployPlan: 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.
Try it out
Section titled “Try it out”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.