Servers
<qf-form> talks to a small API. Its contract is spec/openapi.yaml:
| Request | Does |
|---|---|
GET /forms/{id} |
The latest version of a form. |
GET /forms/{id}/versions/{version} |
A fixed version. It never changes, so it is cached for a year. |
POST /forms/{id}/versions/{version}/datasources/{lookup} |
Runs a lookup the form declares. |
POST /forms/{id}/versions/{version}/submissions |
Checks and accepts a submission. |
Two SDKs implement it, with the same behaviour and the same tests.
ASP.NET Core
builder.Services.AddQuillflow()
.AddFormsFromDirectory("forms")
.AddConnector("company", async (p, ctx, ct) => await cvr.LookupAsync((string)p["cvr"]!, ctx.User, ct))
.OnSubmit(async (submission, ctx, ct) => new SubmissionReceipt(await cases.CreateAsync(submission.Data, ct)));
app.MapQuillflow("/api").RequireAuthorization().RequireRateLimiting("forms");MapQuillflow returns an ordinary route group, so authentication, authorization, rate limiting and
CORS are added the usual way. ctx.User is the signed-in user. Plug in your own storage with
UseFormStore<T>() (definitions in a database) and UseIdempotencyStore<T>() (shared between servers).
Node
import { createHandler, HttpError, memoryFormStore, toNodeListener } from "@quillflow/server-node";
const handler = createHandler({
basePath: "/api",
forms: memoryFormStore(definitions),
connectors: { company: ({ cvr }, { principal }) => cvrRegister.lookup(String(cvr), principal) },
onSubmit: async (submission, { principal }) => ({ reference: await cases.create(submission.data, principal) }),
authenticate: (request) => {
const user = sessionUser(request);
if (!user) throw new HttpError(401, "Log in first");
return user;
},
});
http.createServer(toNodeListener(handler)); // node:http or Express
export const POST = (request: Request) => handler(request); // or any fetch-style frameworkThe handler takes a standard Request and returns a Response, so it also runs in Hono and
Next.js route handlers.
What happens to a submission
- The body must be JSON and at most 1 MB (configurable).
- The form's own engine loads the data and runs every lookup again on the server.
- Every rule is checked again. Problems come back as
422with one message per field, in the caller's language (fromAccept-Language). - Only the data that is visible under the form's rules is kept. Calculated values are recomputed, and unknown keys are dropped.
- Your handler gets that cleaned data and returns a reference, such as a case number.
Retries are safe. <qf-form> sends an Idempotency-Key. The same key with the same data returns the
original receipt, even when two requests arrive at the same moment; the same key with different data
is refused.
Errors use problem details (application/problem+json). Internal errors are logged and never sent to the browser.