quickgql — documentation for AI agents / LLMs GOAL quickgql is a quick, AI-driven way to spin up a GraphQL API for testing and building proofs of concept. There is no config file, no Go code to write, and no server restart. You describe a schema (as JSON, not SDL) and resolvers (as small JavaScript function bodies) through an admin GraphQL API, and a fully working GraphQL endpoint appears immediately at its own URL, backed by its own persistent database. This document is plain text on purpose so it is cheap and easy for an LLM agent to read in full and act on directly. BASE URL All URLs below are already resolved against this server's actual base URL: https://quickgql.wricardo.net TOP-LEVEL CONCEPTS - The admin API lives at https://quickgql.wricardo.net/graphql. It has no auth — this is a dev tool, not for production traffic. - Through the admin API you create "endpoints" by name. Each endpoint gets its own GraphQL schema, its own resolvers, and its own database, mounted at https://quickgql.wricardo.net//graphql. - Endpoint names must match ^[a-zA-Z0-9_-]+$. - Every endpoint (and the admin API itself) serves GraphiQL on a plain GET, so a human can browse it in a browser; POST with a JSON body is how you execute operations, including from curl or an agent. - Schemas are described as data (JSON type definitions), not SDL strings. Field/argument type references still use SDL shorthand strings inside that JSON, e.g. "String!", "[Int]", "ID". - Resolvers are JavaScript function bodies, executed in an embedded sandboxed JS runtime (goja) — no Node dependency. Fields with no resolver just read the matching key off the parent object, so only computed/DB-backed fields need JS. - Mutations that change an endpoint's schema/resolvers are validated before they are persisted. A bad patch is rejected, the previous definition is left untouched, and the endpoint keeps serving with its old schema. - Changes take effect immediately — no restart. ADMIN GRAPHQL API (https://quickgql.wricardo.net/graphql) Queries: endpoints: [Endpoint!]! endpoint(name: String!): Endpoint Mutations: createEndpoint(name: String!, types: [TypeDefInput!], resolvers: [ResolverEntryInput!]): Endpoint! deleteEndpoint(name: String!): Boolean! setType(name: String!, type: TypeDefInput!): Endpoint! # add or replace a type removeType(name: String!, typeName: String!): Endpoint! setField(name: String!, typeName: String!, field: FieldDefInput!): Endpoint! # add or replace a field removeField(name: String!, typeName: String!, fieldName: String!): Endpoint! setResolver(name: String!, key: String!, body: String!): Endpoint! # key is "Type.field" removeResolver(name: String!, key: String!): Endpoint! Endpoint object: type Endpoint { name: String! path: String! # derived: //graphql types: [TypeDef!]! resolvers: [ResolverEntry!]! collections: [String!]! # collections in this endpoint's own database documents(collection: String!, limit: Int = 100): JSON! # raw documents from one of those collections } Schema-description types: enum TypeKind { OBJECT INPUT_OBJECT ENUM } # INTERFACE/UNION not implemented yet type TypeDef { kind: TypeKind!, name: String!, fields: [FieldDef!], values: [String!] } type FieldDef { name: String!, type: String!, args: [ArgDef!] } type ArgDef { name: String!, type: String! } type ResolverEntry { key: String!, body: String! } # TypeDefInput / FieldDefInput / ArgDefInput / ResolverEntryInput mirror these on the input side Naming convention: in every admin mutation, "name" always identifies the endpoint; "typeName" always identifies a type inside that endpoint. A Query OBJECT type is required in every endpoint's "types". A Mutation OBJECT type is optional. RESOLVER JAVASCRIPT API Each resolver body runs as the body of `function(args, parent, ctx) { ... }` and must `return` the field's value. Inside the body you have: args - the field's GraphQL arguments, as a plain object parent - the parent object, for nested/relational fields ctx - the request context, e.g. ctx.headers['User-Agent'] db - this endpoint's own database, MongoDB-style query API: db.insertOne(collection, doc) -> stored doc (auto _id string if missing) db.findOne(collection, filter) -> first match or null db.find(collection, filter) -> array of all matches db.updateOne(collection, filter, update) -> {matchedCount, modifiedCount}; update uses Mongo update-operator syntax e.g. {$set: {...}} db.deleteOne(collection, filter) -> number deleted (0 or 1) db.countDocuments(collection, filter) -> integer count Each endpoint's database is a separate, persistent, embedded MongoDB-wire- protocol instance (mongolite) — data survives schema edits and server restarts as long as --data-dir is unchanged. FULL WALKTHROUGH: CRUD FOR A "users_demo" ENDPOINT All requests below are plain HTTP POST with a JSON body of the form {"query": "...", "variables": {...}}, sent with header Content-Type: application/json. You can use curl, or any HTTP client an agent has available. Step 1 — create the endpoint (admin API, https://quickgql.wricardo.net/graphql): curl -s https://quickgql.wricardo.net/graphql -H 'Content-Type: application/json' -d '{ "query": "mutation C($name:String!,$types:[TypeDefInput!],$resolvers:[ResolverEntryInput!]){ createEndpoint(name:$name,types:$types,resolvers:$resolvers){ name path } }", "variables": { "name": "users_demo", "types": [ { "kind": "OBJECT", "name": "User", "fields": [ { "name": "id", "type": "ID!" }, { "name": "name", "type": "String!" }, { "name": "email", "type": "String!" } ] }, { "kind": "OBJECT", "name": "Query", "fields": [ { "name": "users", "type": "[User!]!" }, { "name": "user", "type": "User", "args": [{ "name": "id", "type": "ID!" }] } ] }, { "kind": "OBJECT", "name": "Mutation", "fields": [ { "name": "createUser", "type": "User!", "args": [{ "name": "name", "type": "String!" }, { "name": "email", "type": "String!" }] }, { "name": "updateUser", "type": "User!", "args": [{ "name": "id", "type": "ID!" }, { "name": "name", "type": "String" }, { "name": "email", "type": "String" }] }, { "name": "deleteUser", "type": "Boolean!", "args": [{ "name": "id", "type": "ID!" }] } ] } ], "resolvers": [ { "key": "Query.users", "body": "var docs = db.find(\"users\", {}); docs.forEach(function(d){ d.id = d._id; }); return docs;" }, { "key": "Query.user", "body": "var d = db.findOne(\"users\", {_id: args.id}); if (!d) return null; d.id = d._id; return d;" }, { "key": "Mutation.createUser", "body": "var d = db.insertOne(\"users\", {name: args.name, email: args.email}); d.id = d._id; return d;" }, { "key": "Mutation.updateUser", "body": "var set = {}; if (args.name) set.name = args.name; if (args.email) set.email = args.email; db.updateOne(\"users\", {_id: args.id}, {$set: set}); var d = db.findOne(\"users\", {_id: args.id}); d.id = d._id; return d;" }, { "key": "Mutation.deleteUser", "body": "return db.deleteOne(\"users\", {_id: args.id}) > 0;" } ] } }' Expect: {"data":{"createEndpoint":{"name":"users_demo","path":"/users_demo/graphql"}}} It is live immediately at https://quickgql.wricardo.net/users_demo/graphql. Step 2 — verify it's mounted (admin API): curl -s https://quickgql.wricardo.net/graphql -H 'Content-Type: application/json' -d '{ "query": "{ endpoint(name:\"users_demo\") { name path collections } }" }' From here on, requests target the endpoint's own URL — https://quickgql.wricardo.net/users_demo/graphql — not the admin /graphql used above. Step 3 — create a user: curl -s https://quickgql.wricardo.net/users_demo/graphql -H 'Content-Type: application/json' -d '{ "query": "mutation($n:String!,$e:String!){ createUser(name:$n, email:$e) { id name email } }", "variables": { "n": "Ada Lovelace", "e": "ada@example.com" } }' Note the returned "id" — used below (example: "abc123"). Step 4 — list / read: curl -s https://quickgql.wricardo.net/users_demo/graphql -H 'Content-Type: application/json' -d '{ "query": "{ users { id name email } }" }' curl -s https://quickgql.wricardo.net/users_demo/graphql -H 'Content-Type: application/json' -d '{ "query": "query($id:ID!){ user(id:$id) { id name email } }", "variables": { "id": "abc123" } }' Step 5 — update: curl -s https://quickgql.wricardo.net/users_demo/graphql -H 'Content-Type: application/json' -d '{ "query": "mutation($id:ID!,$e:String!){ updateUser(id:$id, email:$e) { id name email } }", "variables": { "id": "abc123", "e": "ada@newmail.com" } }' Step 6 — delete: curl -s https://quickgql.wricardo.net/users_demo/graphql -H 'Content-Type: application/json' -d '{ "query": "mutation($id:ID!){ deleteUser(id:$id) }", "variables": { "id": "abc123" } }' Confirm gone: curl -s https://quickgql.wricardo.net/users_demo/graphql -H 'Content-Type: application/json' -d '{ "query": "{ users { id name email } }" }' Step 7 — clean up, delete the endpoint itself (admin API): curl -s https://quickgql.wricardo.net/graphql -H 'Content-Type: application/json' -d '{ "query": "mutation($n:String!){ deleteEndpoint(name:$n) }", "variables": { "n": "users_demo" } }' Note: this removes the endpoint from the mux and from the meta database, but its underlying data file and mongolite instance are left on disk (this is a known limitation, tracked in the project's roadmap). PATCHING A SCHEMA WITHOUT RECREATING IT Add a computed field to an existing endpoint and give it a resolver, without touching anything else: curl -s https://quickgql.wricardo.net/graphql -H 'Content-Type: application/json' -d '{ "query": "mutation($f:FieldDefInput!){ setField(name:\"users_demo\", typeName:\"User\", field:$f){ path } }", "variables": { "f": { "name": "handle", "type": "String!" } } }' curl -s https://quickgql.wricardo.net/graphql -H 'Content-Type: application/json' -d '{ "query": "mutation{ setResolver(name:\"users_demo\", key:\"User.handle\", body:\"return parent.name.split(\\\" \\\")[0].toLowerCase();\"){ path } }" }' This remounts users_demo atomically. If the patch would break the schema, it is rejected and the previously working endpoint keeps serving unchanged. INSPECTING RAW DATA THROUGH THE ADMIN API curl -s https://quickgql.wricardo.net/graphql -H 'Content-Type: application/json' -d '{ "query": "{ endpoint(name:\"users_demo\") { collections documents(collection:\"users\", limit:10) } }" }' CURRENT LIMITATIONS (do not rely on these working yet) - Deleting an endpoint leaves its data file and mongolite instance running. - Endpoints cannot be renamed. - INTERFACE / UNION type kinds are not implemented. - The admin API cannot filter/write documents directly (only browse them); all data writes go through an endpoint's own resolvers.