Google Forms

Lightweight, dependency-free in-process fake of the Google Forms API v1 for zero-cost local agent tests.

Default Port

4625

Implemented Operations

Forms

googleapis methodHTTP endpoint
forms.forms.createPOST /v1/forms
forms.forms.getGET /v1/forms/{formId}
forms.forms.batchUpdatePOST /v1/forms/{formId}:batchUpdate
forms.forms.setPublishSettingsPOST /v1/forms/{formId}:setPublishSettings

Responses

googleapis methodHTTP endpoint
forms.forms.responses.getGET /v1/forms/{formId}/responses/{responseId}
forms.forms.responses.listGET /v1/forms/{formId}/responses

Watches

googleapis methodHTTP endpoint
forms.forms.watches.createPOST /v1/forms/{formId}/watches
forms.forms.watches.listGET /v1/forms/{formId}/watches
forms.forms.watches.renewPOST /v1/forms/{formId}/watches/{watchId}:renew
forms.forms.watches.deleteDELETE /v1/forms/{formId}/watches/{watchId}

Parlel Helpers

helperHTTP endpoint
HealthGET /_parlel/health
Reset ephemeral statePOST /_parlel/reset
Seed a response for read-only Forms response APIsPOST /_parlel/forms/{formId}/responses

The server also accepts /forms/v1/... as an alias for /v1/..., matching the product-prefixed root URL style used by the other Google service fakes.

Quick Start

import { google } from "googleapis";
import { GoogleFormsServer } from "./services/google-forms/src/server.js";

const server = new GoogleFormsServer(4625);
await server.start();

const forms = google.forms({
  version: "v1",
  rootUrl: "http://127.0.0.1:4625/",
  auth: "not-used-by-parlel",
});

const created = await forms.forms.create({
  requestBody: { info: { title: "Local survey" } },
});

await forms.forms.batchUpdate({
  formId: created.data.formId,
  requestBody: {
    requests: [
      {
        createItem: {
          location: { index: 0 },
          item: {
            title: "Name",
            questionItem: { question: { textQuestion: {} } },
          },
        },
      },
    ],
  },
});

await server.stop();

Supported Features

FeatureStatusNotes
In-memory formsSupportedformId, revisionId, responderUri, info, settings, items, and publishSettings are returned in Forms-shaped JSON.
forms.createSupportedAccepts info.title, optional info.documentTitle, and unpublished=true; rejects disallowed create-time fields such as items and settings.
forms.getSupportedReturns the current in-memory form.
forms.batchUpdateSupportedSupports all request variants exposed by Google Forms v1 discovery: updateFormInfo, updateSettings, createItem, moveItem, deleteItem, and updateItem. Applies batches atomically.
forms.setPublishSettingsSupportedUpdates publishSettings.publishState; rejects accepting responses while unpublished.
Response readsSupportedresponses.get and responses.list read seeded in-memory responses. List supports pageSize, pageToken, and timestamp filters timestamp > RFC3339 and timestamp >= RFC3339.
Response writes via Google APIIntentionally unsupportedThe public Google Forms API does not expose a response create endpoint. Use the parlel-only seeding helper or server.addResponse() in tests.
Watch lifecycleSupportedCreate/list/renew/delete are implemented in memory with event-type uniqueness per form and seven-day expiration timestamps.
Pub/Sub deliveryIntentionally unsupportedWatches store target metadata but do not publish messages. Pair with the parlel Pub/Sub fake at the application layer if needed.
OAuth, Drive permissions, linked Sheets, real Forms UIIntentionally unsupportedAuth headers are ignored; access control and side effects outside this process are not modeled.
PersistenceIntentionally unsupportedState is ephemeral and reset by POST /_parlel/reset or server.reset().

Error Shape

Errors use the Google JSON envelope used across parlel Google fakes:

{
  "error": {
    "code": 404,
    "message": "Form not found",
    "status": "NOT_FOUND",
    "errors": [
      {
        "message": "Form not found",
        "domain": "global",
        "reason": "notFound"
      }
    ]
  }
}

Common statuses returned:

HTTP statusGoogle statusTypical reason
400INVALID_ARGUMENTInvalid JSON, invalid field mask, missing required field, stale requiredRevisionId, invalid response filter, invalid watch ID.
404NOT_FOUNDMissing form, response, watch, or unsupported route.
405METHOD_NOT_ALLOWEDValid route with unsupported HTTP method.
409ALREADY_EXISTSDuplicate watch ID or duplicate watch event type on the same form.
500INTERNALUnexpected emulator error.
<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

GOOGLE_FORMS_EMULATOR_HOST=http://parlel-bridge:4625
GOOGLE_CLOUD_PROJECT=parlel
GCLOUD_PROJECT=parlel
<!-- parlel:testenv:end -->