| path | docs/api/GraphQL_API.mdx | |||||
|---|---|---|---|---|---|---|
| title | GraphQL API | |||||
| description | Complete GraphQL API reference for querying and mutating data in SveltyCMS with dynamic schema generation. | |||||
| order | 11 | |||||
| icon | mdi:graphql | |||||
| author | admin | |||||
| created | 2025-10-05 | |||||
| updated | 2025-11-08 | |||||
| tags |
|
The GraphQL API provides a powerful, flexible query language for accessing and manipulating data in SveltyCMS. It features dynamic schema generation based on your collections, widgets, and content structure.
Endpoint: /api/graphql
Method: POST
Content-Type: application/json
Key Features:
- ✅ Dynamic schema generation from collections
- ✅ Type-safe queries with auto-completion
- ✅ Pagination support for all queries
- ✅ Real-time subscriptions via WebSocket
- ✅ Multi-tenant data isolation
- ✅ Redis caching integration
- ✅ Permission-based access control
- ✅ Introspection and GraphiQL playground
All GraphQL requests require authentication via session cookie:
Cookie: session=your-session-cookie// Session cookie automatically sent
fetch('/api/graphql', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});curl -X POST https://cms.example.com/api/graphql \
-H "Authorization: Bearer your-api-token" \
-H "Content-Type: application/json" \
-d '{"query":"{ users { _id email } }"}'Unauthorized Response (401):
{
"error": "Unauthorized",
"message": "You must be logged in to access the GraphQL endpoint."
}Access the interactive GraphQL playground for development:
https://your-domain.com/api/graphql
The playground provides:
- 📖 Full schema documentation browser
- ✨ Auto-completion and syntax highlighting
- ✔️ Real-time query validation
- 🔍 Schema introspection
- 📊 Query performance metrics
- 🔌 WebSocket subscription testing
The GraphQL schema is dynamically generated based on your:
- Collections - Each collection becomes a queryable type
- Widgets - Widget schemas define field types
- Custom Fields - Extracted and nested fields are supported
- Media Types - Images, documents, audio, video, remote
# Auto-generated from "Posts" collection
type Posts_abc123 {
_id: String
title: String
content: String
author: User
tags: [String]
status: String
createdAt: String
updatedAt: String
createdBy: String
updatedBy: String
}
type Query {
# Collection queries (dynamically generated)
Posts_abc123: [Posts_abc123]
Articles_def456: [Articles_def456]
# User queries
users(pagination: PaginationInput): [User]
# Media queries
mediaImages(pagination: PaginationInput): [MediaImage]
mediaDocuments(pagination: PaginationInput): [MediaDocument]
mediaAudio(pagination: PaginationInput): [MediaAudio]
mediaVideos(pagination: PaginationInput): [MediaVideo]
mediaRemote(pagination: PaginationInput): [MediaRemote]
# Permission queries
accessManagementPermission: AccessManagementPermission
}
input PaginationInput {
page: Int = 1
limit: Int = 50
}query GetPosts {
posts(limit: 10, filter: { status: "published" }) {
_id
title
content
author {
_id
username
email
}
createdAt
updatedAt
}
}Response:
{
"data": {
"posts": [
{
"_id": "post123",
"title": "Hello World",
"content": "Post content...",
"author": {
"_id": "user123",
"username": "john_doe",
"email": "john@example.com"
},
"createdAt": "2025-10-05T14:30:00Z",
"updatedAt": "2025-10-05T14:30:00Z"
}
]
}
}query GetPost($id: ID!) {
post(id: $id) {
_id
title
content
status
author {
username
}
}
}Variables:
{
"id": "post123"
}query FilteredPosts {
posts(filter: { status: "published", author: "user123" }, limit: 20, skip: 0) {
_id
title
createdAt
}
}query Me {
me {
_id
username
email
role
permissions
createdAt
}
}query GetUsers {
users(filter: { role: "editor" }, limit: 10) {
_id
username
email
role
blocked
}
}query GetUser($id: ID!) {
user(id: $id) {
_id
username
email
role
createdAt
lastLogin
}
}query GetMedia {
media(limit: 20) {
_id
filename
url
mimeType
size
uploadedBy {
username
}
uploadedAt
}
}query GetMediaFile($id: ID!) {
mediaFile(id: $id) {
_id
filename
url
mimeType
size
width
height
metadata
}
}mutation CreatePost($input: PostInput!) {
createPost(input: $input) {
_id
title
content
status
createdAt
}
}Variables:
{
"input": {
"title": "New Post",
"content": "Post content here...",
"status": "draft",
"author": "user123"
}
}mutation UpdatePost($id: ID!, $input: PostInput!) {
updatePost(id: $id, input: $input) {
_id
title
updatedAt
}
}Variables:
{
"id": "post123",
"input": {
"title": "Updated Title",
"status": "published"
}
}mutation DeletePost($id: ID!) {
deletePost(id: $id)
}SveltyCMS automatically generates GraphQL types for each collection:
type Post {
_id: ID!
title: String!
content: String
excerpt: String
status: String
author: User
createdAt: DateTime
updatedAt: DateTime
}
input PostInput {
title: String!
content: String
excerpt: String
status: String
author: ID
}type Page {
_id: ID!
title: String!
slug: String!
content: String
template: String
createdAt: DateTime
}
input PageInput {
title: String!
slug: String!
content: String
template: String
}GraphQL queries respect user permissions:
// Admin users
query {
posts { _id title status } // ✅ All posts
}
// Editor users
query {
posts { _id title status } // ✅ Published + own drafts
}
// Viewer users
query {
posts { _id title } // ✅ Published only
}Unauthorized Query:
{
"errors": [
{
"message": "Unauthorized: Insufficient permissions",
"extensions": {
"code": "FORBIDDEN"
}
}
]
}GraphQL queries are cached for performance:
// Cached queries (5 minutes)
query GetPosts {
posts { _id title }
}
// Cache bypassed for authenticated requests with mutations
mutation CreatePost {
createPost(input: {...}) { _id }
}Cache Keys:
- Query hash + user ID
- Collection name + filters
- Tenant ID (multi-tenant mode)
Cache Invalidation:
- On mutations (create, update, delete)
- Manual cache clear
- TTL expiration (5 minutes default)
GraphQL resolvers use database adapters:
// Collections resolver
const posts = await dbAdapter.crud.findMany('posts', filter, {
limit,
skip,
sort: { createdAt: -1 }
});
// User resolver
const user = await dbAdapter.auth.user.findOne({ _id: userId });
// Media resolver
const media = await dbAdapter.media.files.getByFolder(folder);No direct database queries - all through adapter interface.
When multi-tenant mode is enabled:
- Queries automatically scoped to tenant
- Users can only access own tenant data
- Cross-tenant queries blocked
Tenant Filtering:
const baseFilter = MULTI_TENANT ? { tenantId } : {};
const results = await dbAdapter.crud.findMany(collection, { ...filter, ...baseFilter });GraphQL errors follow standard format:
{
"errors": [
{
"message": "Post not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["post"],
"extensions": {
"code": "NOT_FOUND",
"id": "post123"
}
}
],
"data": {
"post": null
}
}Error Codes:
UNAUTHENTICATED- Not logged inFORBIDDEN- Insufficient permissionsNOT_FOUND- Resource doesn't existBAD_USER_INPUT- Invalid input dataINTERNAL_SERVER_ERROR- Server error
query PostWithAuthorAndComments {
post(id: "post123") {
title
author {
username
email
}
comments {
_id
content
author {
username
}
}
}
}query PostStats {
postsCount: posts(filter: { status: "published" }) {
_id
}
}query MultipleQueries {
published: posts(filter: { status: "published" }) {
_id
title
}
drafts: posts(filter: { status: "draft" }) {
_id
title
}
}async function queryGraphQL(query, variables = {}) {
const response = await fetch('/api/graphql', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ query, variables })
});
const { data, errors } = await response.json();
if (errors) {
throw new Error(errors[0].message);
}
return data;
}
// Usage
const data = await queryGraphQL(`
query GetPosts {
posts(limit: 10) {
_id
title
}
}
`);import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
const client = new ApolloClient({
uri: '/api/graphql',
cache: new InMemoryCache(),
credentials: 'include'
});
const { data } = await client.query({
query: gql`
query GetPosts {
posts {
_id
title
}
}
`
});import { createClient } from 'urql';
const client = createClient({
url: '/api/graphql',
fetchOptions: {
credentials: 'include'
}
});
const result = await client
.query(
`
query GetPosts {
posts {
_id
title
}
}
`
)
.toPromise();Only request fields you need:
# ❌ Requesting all fields
query {
posts {
_id
title
content
excerpt
author { ... }
createdAt
updatedAt
}
}
# ✅ Request only needed fields
query {
posts {
_id
title
}
}Use pagination for large datasets:
query PaginatedPosts($limit: Int!, $skip: Int!) {
posts(limit: $limit, skip: $skip) {
_id
title
}
}Batch multiple queries:
query BatchQuery {
posts {
_id
title
}
users {
_id
username
}
media {
_id
filename
}
}All inputs are validated:
mutation CreatePost($input: PostInput!) {
createPost(input: $input) {
_id
}
}Prevents deeply nested queries:
# ❌ Too deep (blocked)
query {
post {
author {
posts {
author {
posts { ... }
}
}
}
}
}GraphQL endpoint is rate limited:
- 60 queries/minute per user
- 100 mutations/hour per user
GraphQL subscriptions enable real-time data updates via WebSocket connections.
type Subscription {
postAdded: Post
}subscription OnPostAdded {
postAdded {
_id
title
content
createdAt
author {
username
}
}
}Subscriptions require a WebSocket connection on port 3001:
- Endpoint:
ws://localhost:3001/api/graphql - Protocol:
graphql-ws - Authentication: Session ID, Cookie, or Bearer Token
import { createClient } from 'graphql-ws';
const client = createClient({
url: 'ws://localhost:3001/api/graphql',
connectionParams: {
cookie: document.cookie
}
});
client.subscribe(
{
query: `
subscription {
postAdded {
_id
title
}
}
`
},
{
next: (data) => console.log('New post:', data),
error: (error) => console.error('Error:', error)
}
);For complete WebSocket authentication methods and implementation details, see:
Comprehensive test coverage for all GraphQL features.
tests/bun/api/graphql.test.ts
# Run all GraphQL tests
bun test tests/bun/api/graphql.test.ts
# Run specific test suite
bun test tests/bun/api/graphql.test.ts -t "User Queries"
# Run with coverage
bun test --coverage tests/bun/api/graphql.test.tsAuthentication & Authorization (2 tests)
- ✅ Reject unauthenticated requests
- ✅ Accept authenticated requests
User Queries (3 tests)
- ✅ Fetch users list
- ✅ Fetch users with pagination
- ✅ Verify sensitive fields not exposed
Media Queries (5 tests)
- ✅ Fetch media images
- ✅ Fetch media documents
- ✅ Fetch media with pagination
- ✅ Fetch different media types
- ✅ Verify media type isolation
Schema Introspection (2 tests)
- ✅ Support introspection queries
- ✅ List available query fields
Error Handling (3 tests)
- ✅ Return errors for invalid queries
- ✅ Return errors for malformed queries
- ✅ Handle missing required fields
Complex Queries (3 tests)
- ✅ Support multiple queries in one request
- ✅ Support query aliases
- ✅ Support fragments
Multi-Tenant Support (1 test)
- ✅ Scope queries to tenant context
Performance & Caching (2 tests)
- ✅ Handle large pagination requests
- ✅ Execute queries efficiently (<5s)
Total: 21 test cases covering all GraphQL features
describe('User Queries', () => {
it('should fetch users with pagination', async () => {
const query = `
query GetUsers($pagination: PaginationInput) {
users(pagination: $pagination) {
_id
email
username
}
}
`;
const response = await executeGraphQL(
query,
{
pagination: {
page: 1,
limit: 5
}
},
authCookie
);
expect(response.status).toBe(200);
const result = await response.json();
expect(result.data.users).toBeDefined();
expect(Array.isArray(result.data.users)).toBe(true);
});
});# Test user query
curl -X POST https://cms.example.com/api/graphql \
-H "Cookie: session=your-session-cookie" \
-H "Content-Type: application/json" \
-d '{
"query": "query { users { _id email username } }"
}'
# Test media query with pagination
curl -X POST https://cms.example.com/api/graphql \
-H "Cookie: session=your-session-cookie" \
-H "Content-Type: application/json" \
-d '{
"query": "query($pagination: PaginationInput) { mediaImages(pagination: $pagination) { _id url } }",
"variables": { "pagination": { "page": 1, "limit": 10 } }
}'
# Test introspection
curl -X POST https://cms.example.com/api/graphql \
-H "Cookie: session=your-session-cookie" \
-H "Content-Type: application/json" \
-d '{
"query": "query { __schema { types { name } } }"
}'# Test query performance
time curl -X POST https://cms.example.com/api/graphql \
-H "Cookie: session=your-session-cookie" \
-H "Content-Type: application/json" \
-d '{
"query": "query { users { _id } mediaImages { _id } }"
}'Test GraphQL with other API endpoints:
// 1. Create user via REST API
const userResponse = await fetch('/api/user/createUser', {
method: 'POST',
body: JSON.stringify({ email, username, password })
});
// 2. Query user via GraphQL
const graphqlResponse = await fetch('/api/graphql', {
method: 'POST',
body: JSON.stringify({
query: `query { users { _id email } }`
})
});
// 3. Verify user exists in GraphQL response
const result = await graphqlResponse.json();
const user = result.data.users.find((u) => u.email === email);
expect(user).toBeDefined();- GraphQL WebSocket Subscriptions - Real-time updates guide
- Collection API
- User Management API
- Media API
- Testing Documentation - Complete testing guide
For implementation details, see:
src/routes/api/graphql/+server.ts- Main GraphQL endpointsrc/routes/api/graphql/resolvers/collections.ts- Dynamic collection schema generationsrc/routes/api/graphql/resolvers/users.ts- User queries and type definitionssrc/routes/api/graphql/resolvers/media.ts- Media queries for all media typestests/bun/api/graphql.test.ts- Complete test suite