Claude-skill-registry keystonejs
Builds content APIs with KeystoneJS, the Node.js headless CMS with GraphQL. Use when creating admin UIs and APIs with automatic CRUD operations, relationships, access control, and TypeScript support.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/keystonejs" ~/.claude/skills/majiayu000-claude-skill-registry-keystonejs && rm -rf "$T"
manifest:
skills/data/keystonejs/SKILL.mdsource content
KeystoneJS
Open-source Node.js headless CMS that auto-generates GraphQL API and Admin UI from your schema. TypeScript-first with powerful access control.
Quick Start
npm create keystone-app@latest my-app cd my-app npm run dev
Opens:
- Admin UI:
http://localhost:3000 - GraphQL Playground:
http://localhost:3000/api/graphql
Configuration
// keystone.ts import { config } from '@keystone-6/core'; import { lists } from './schema'; import { withAuth, session } from './auth'; export default withAuth( config({ db: { provider: 'postgresql', // or 'sqlite', 'mysql' url: process.env.DATABASE_URL!, }, lists, session, ui: { isAccessAllowed: (context) => !!context.session?.data, }, }) );
Schema Definition
// schema.ts import { list } from '@keystone-6/core'; import { text, timestamp, relationship, select, checkbox, password } from '@keystone-6/core/fields'; import { document } from '@keystone-6/fields-document'; export const lists = { User: list({ fields: { name: text({ validation: { isRequired: true } }), email: text({ validation: { isRequired: true }, isIndexed: 'unique', }), password: password({ validation: { isRequired: true } }), posts: relationship({ ref: 'Post.author', many: true }), }, }), Post: list({ fields: { title: text({ validation: { isRequired: true } }), slug: text({ isIndexed: 'unique' }), status: select({ options: [ { label: 'Draft', value: 'draft' }, { label: 'Published', value: 'published' }, ], defaultValue: 'draft', ui: { displayMode: 'segmented-control' }, }), content: document({ formatting: true, links: true, dividers: true, layouts: [ [1, 1], [1, 1, 1], ], }), publishedAt: timestamp(), author: relationship({ ref: 'User.posts', ui: { displayMode: 'cards', cardFields: ['name', 'email'], inlineCreate: { fields: ['name', 'email'] }, }, }), tags: relationship({ ref: 'Tag.posts', many: true, ui: { displayMode: 'select', labelField: 'name', }, }), }, hooks: { resolveInput: async ({ resolvedData, inputData }) => { // Auto-generate slug from title if (inputData.title && !inputData.slug) { resolvedData.slug = inputData.title.toLowerCase().replace(/\s+/g, '-'); } return resolvedData; }, }, }), Tag: list({ fields: { name: text({ validation: { isRequired: true } }), posts: relationship({ ref: 'Post.tags', many: true }), }, }), };
Field Types
import { text, password, integer, float, decimal, checkbox, select, multiselect, timestamp, calendarDay, json, relationship, file, image, } from '@keystone-6/core/fields'; import { document } from '@keystone-6/fields-document'; // Text text({ validation: { isRequired: true, length: { max: 255 } } }) // Password (hashed) password({ validation: { isRequired: true } }) // Numbers integer({ defaultValue: 0 }) float() decimal({ precision: 10, scale: 2 }) // Boolean checkbox({ defaultValue: false }) // Select select({ options: [ { label: 'Draft', value: 'draft' }, { label: 'Published', value: 'published' }, ], defaultValue: 'draft', }) // Multi-select multiselect({ options: [ { label: 'React', value: 'react' }, { label: 'Vue', value: 'vue' }, { label: 'Svelte', value: 'svelte' }, ], }) // Dates timestamp() calendarDay() // JSON json() // Relationship relationship({ ref: 'Post.author', // Related list.field many: false, // One or many }) // File upload file({ storage: 's3' }) // Image with transforms image({ storage: 'local' }) // Rich text (Document) document({ formatting: true, links: true, dividers: true, layouts: [[1, 1]], })
Access Control
import { list } from '@keystone-6/core'; export const lists = { Post: list({ access: { operation: { query: () => true, // Anyone can query create: ({ session }) => !!session, // Logged in only update: ({ session }) => !!session, delete: ({ session }) => session?.data.isAdmin, // Admin only }, filter: { query: ({ session }) => { // Non-logged in users only see published if (!session) { return { status: { equals: 'published' } }; } return true; // Logged in see all }, }, item: { update: ({ session, item }) => { // Users can only update their own posts if (session?.data.isAdmin) return true; return session?.itemId === item.authorId; }, }, }, fields: {/* ... */}, }), };
Hooks
export const lists = { Post: list({ hooks: { // Before validation resolveInput: async ({ resolvedData, inputData, item, context }) => { // Modify data before saving if (inputData.title) { resolvedData.slug = inputData.title.toLowerCase().replace(/\s+/g, '-'); } return resolvedData; }, // Validation validateInput: async ({ resolvedData, addValidationError }) => { if (resolvedData.title?.length < 3) { addValidationError('Title must be at least 3 characters'); } }, // Before save beforeOperation: async ({ operation, resolvedData, context }) => { if (operation === 'create') { // Set author to current user resolvedData.author = { connect: { id: context.session?.itemId } }; } }, // After save afterOperation: async ({ operation, item, context }) => { if (operation === 'create') { console.log(`New post created: ${item.title}`); // Send notification, revalidate cache, etc. } }, }, fields: {/* ... */}, }), };
GraphQL API
Auto-generated based on your schema.
Queries
# Get all posts query { posts { id title status author { name } } } # Get single post query { post(where: { id: "123" }) { title content { document } } } # Filter and sort query { posts( where: { status: { equals: "published" } title: { contains: "javascript" } } orderBy: { publishedAt: desc } take: 10 skip: 0 ) { id title publishedAt } } # Count query { postsCount(where: { status: { equals: "published" } }) }
Mutations
# Create mutation { createPost(data: { title: "New Post" content: { document: [...] } author: { connect: { id: "user-id" } } }) { id title } } # Update mutation { updatePost( where: { id: "123" } data: { title: "Updated Title" } ) { id title } } # Delete mutation { deletePost(where: { id: "123" }) { id } } # Create many mutation { createPosts(data: [ { title: "Post 1" }, { title: "Post 2" } ]) { id title } }
Filter Operators
where: { # Equality title: { equals: "Hello" } title: { not: { equals: "Hello" } } # String matching title: { contains: "react" } title: { startsWith: "How to" } title: { endsWith: "Guide" } # Comparison views: { gt: 100 } views: { gte: 100 } views: { lt: 1000 } views: { lte: 1000 } # List status: { in: ["draft", "review"] } status: { notIn: ["archived"] } # Relationship author: { id: { equals: "user-id" } } author: { name: { contains: "John" } } tags: { some: { name: { equals: "react" } } } tags: { every: { name: { in: ["react", "javascript"] } } } tags: { none: { name: { equals: "deprecated" } } } # Logical AND: [{ status: { equals: "published" } }, { featured: { equals: true } }] OR: [{ status: { equals: "published" } }, { author: { id: { equals: "me" } } }] NOT: { status: { equals: "archived" } } }
Query API (Server-Side)
// In resolvers, hooks, or custom API routes const posts = await context.query.Post.findMany({ where: { status: { equals: 'published' } }, orderBy: { publishedAt: 'desc' }, take: 10, query: 'id title slug author { name }', }); const post = await context.query.Post.findOne({ where: { id: 'post-id' }, query: 'id title content { document } author { name email }', }); // Create const newPost = await context.query.Post.createOne({ data: { title: 'New Post', author: { connect: { id: context.session?.itemId } }, }, query: 'id title', }); // Update await context.query.Post.updateOne({ where: { id: 'post-id' }, data: { title: 'Updated' }, }); // Delete await context.query.Post.deleteOne({ where: { id: 'post-id' }, });
Authentication
// auth.ts import { createAuth } from '@keystone-6/auth'; import { statelessSessions } from '@keystone-6/core/session'; const { withAuth } = createAuth({ listKey: 'User', identityField: 'email', secretField: 'password', sessionData: 'id name email isAdmin', initFirstItem: { fields: ['name', 'email', 'password'], }, }); const session = statelessSessions({ maxAge: 60 * 60 * 24 * 30, // 30 days secret: process.env.SESSION_SECRET!, }); export { withAuth, session };
File Storage
// keystone.ts import { config } from '@keystone-6/core'; export default config({ storage: { local: { kind: 'local', type: 'file', generateUrl: (path) => `/files${path}`, serverRoute: { path: '/files' }, storagePath: 'public/files', }, s3: { kind: 's3', type: 'file', bucketName: process.env.S3_BUCKET!, region: process.env.S3_REGION!, accessKeyId: process.env.S3_ACCESS_KEY!, secretAccessKey: process.env.S3_SECRET_KEY!, }, }, // ... });
Custom GraphQL
import { graphql } from '@keystone-6/core'; export const lists = { Post: list({ fields: {/* ... */}, }), }; export const extendGraphqlSchema = graphql.extend((base) => ({ query: { featuredPosts: graphql.field({ type: graphql.list(graphql.nonNull(base.object('Post'))), resolve: async (root, args, context) => { return context.query.Post.findMany({ where: { featured: { equals: true } }, orderBy: { publishedAt: 'desc' }, take: 5, }); }, }), }, mutation: { publishPost: graphql.field({ type: base.object('Post'), args: { id: graphql.arg({ type: graphql.nonNull(graphql.ID) }) }, resolve: async (root, { id }, context) => { return context.query.Post.updateOne({ where: { id }, data: { status: 'published', publishedAt: new Date().toISOString(), }, }); }, }), }, }));
Next.js Integration
// lib/keystone.ts const KEYSTONE_URL = process.env.KEYSTONE_URL || 'http://localhost:3000'; export async function fetchGraphQL(query: string, variables = {}) { const response = await fetch(`${KEYSTONE_URL}/api/graphql`, { method: 'POST', 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; } // Typed fetchers export async function getPosts() { const { posts } = await fetchGraphQL(` query { posts(where: { status: { equals: "published" } }, orderBy: { publishedAt: desc }) { id title slug excerpt publishedAt author { name } } } `); return posts; } export async function getPost(slug: string) { const { posts } = await fetchGraphQL(` query GetPost($slug: String!) { posts(where: { slug: { equals: $slug } }) { id title content { document } author { name } } } `, { slug }); return posts[0]; }
Deployment
# Build npm run build # Start production npm run start
Database migrations are handled automatically. For production:
- Use PostgreSQL or MySQL (not SQLite)
- Set
environment variableSESSION_SECRET - Configure storage for uploads (S3 recommended)
Best Practices
- Use TypeScript for full type safety
- Define access control for all lists
- Use hooks for business logic and side effects
- Extend GraphQL for custom operations
- Set up proper relationships with
bidirectional referencesref - Use the Query API for server-side data fetching