Full Tutorial
TypeScript SDK
The FraiseQL TypeScript SDK is a schema authoring SDK: you define GraphQL types, queries, and mutations in TypeScript using decorators, and the FraiseQL compiler generates an optimized GraphQL API backed by your SQL views.
Installation
Section titled “Installation”# npmnpm install fraiseql
# pnpm (recommended)pnpm add fraiseql
# yarnyarn add fraiseql
# bunbun add fraiseqlRequirements: Node.js 18+, TypeScript 5.0+
TypeScript Configuration
Section titled “TypeScript Configuration”Enable decorators in your tsconfig.json:
{ "compilerOptions": { "target": "ES2022", "experimentalDecorators": true, "emitDecoratorMetadata": true, "strict": true }}Core Concepts
Section titled “Core Concepts”FraiseQL TypeScript provides four decorators:
| Decorator | GraphQL Equivalent | Purpose |
|---|---|---|
@type() | type | Define a GraphQL output type |
@input() | input | Define a GraphQL input type |
@query() | Query field | Wire a query to a SQL view |
@mutation() | Mutation field | Define a mutation |
Defining Types
Section titled “Defining Types”Use @type() to define GraphQL output types. Fields map 1:1 to keys in your backing SQL view’s .data JSON object.
import { type } from 'fraiseql';import { ID, Email, Slug, DateTime } from 'fraiseql/scalars';
@type()class User { /** A registered user. */ id: ID; username: string; email: Email; bio: string | null; createdAt: DateTime;}
@type()class Post { /** A blog post. */ id: ID; title: string; slug: Slug; content: string; isPublished: boolean; createdAt: DateTime; updatedAt: DateTime; // Nested types are composed from views at compile time author: User; comments: Comment[];}
@type()class Comment { /** A comment on a post. */ id: ID; content: string; createdAt: DateTime; author: User;}Built-in Scalars
Section titled “Built-in Scalars”import { ID, // UUID, auto-serialized Email, // Validated email address Slug, // URL-safe slug DateTime, // ISO 8601 datetime URL, // Validated URL PhoneNumber, // E.164 phone number} from 'fraiseql/scalars';Defining Inputs
Section titled “Defining Inputs”Use @input() to define GraphQL input types for mutations. Add validation with the validate option:
import { input, validate } from 'fraiseql';import { ID, Email } from 'fraiseql/scalars';
@input()class CreateUserInput { @validate({ minLength: 3, maxLength: 50, pattern: /^[a-z0-9_]+$/ }) username: string;
@validate({ message: 'Must be a valid email address' }) email: Email;
bio?: string;}
@input()class CreatePostInput { @validate({ minLength: 1, maxLength: 200 }) title: string;
content: string;
authorId: ID;
isPublished: boolean = false;}
@input()class UpdatePostInput { id: ID; title?: string; content?: string; isPublished?: boolean;}Validation runs at compile time — invalid input structures cannot be deployed.
Defining Queries
Section titled “Defining Queries”Use @query() to wire queries to SQL views:
import { query, authenticated } from 'fraiseql';import { ID } from 'fraiseql/scalars';
// List query — maps to SELECT * FROM v_post WHERE <args>@query({ sqlSource: 'v_post' })function posts(isPublished?: boolean, authorId?: ID, limit = 20): Post[] { /** * Fetch published posts, optionally filtered by author. * Arguments matching view column names become WHERE clauses automatically. */}
// Single-item query — maps to SELECT * FROM v_post WHERE id = $1@query({ sqlSource: 'v_post', idArg: 'id' })function post(id: ID): Post | null { /** Fetch a single post by ID. */}
// Query with row filter (for user-scoped data)@query({ sqlSource: 'v_post', rowFilter: 'author_id = {currentUserId}' })@authenticatedfunction myPosts(limit = 20): Post[] { /** Fetch the current user's posts. */}Automatic-WHERE
Section titled “Automatic-WHERE”FraiseQL maps query arguments to SQL WHERE clauses automatically. A TypeScript argument named isPublished matching the is_published column in v_post generates:
SELECT data FROM v_post WHERE is_published = true LIMIT 20;No resolver code required.
Defining Mutations
Section titled “Defining Mutations”Use @mutation() to define mutations that execute PostgreSQL functions:
import { mutation, authenticated, requiresScope } from 'fraiseql';import { ID } from 'fraiseql/scalars';
@mutation()function createUser(input: CreateUserInput): User { /** * Create a new user account. * FraiseQL calls fn_create_user($1::jsonb) */}
@mutation()@authenticated@requiresScope('write:posts')function createPost(input: CreatePostInput): Post { /** Create a new blog post. */}
@mutation()@authenticated@requiresScope('write:posts')function publishPost(id: ID): Post { /** Publish a draft post. */}
@mutation()@authenticated@requiresScope('admin:posts')function deletePost(id: ID): boolean { /** Soft-delete a post. */}Authorization
Section titled “Authorization”import { query, mutation, authenticated, requiresScope } from 'fraiseql';
@query({ sqlSource: 'v_post' })@authenticatedfunction posts(limit = 20): Post[] { /** Requires authentication. */}
@mutation()@authenticated@requiresScope('write:posts')function createPost(input: CreatePostInput): Post { /** Requires authentication and write:posts scope. */}Middleware
Section titled “Middleware”import { middleware } from 'fraiseql';
middleware((request, next) => { /** Extract user ID and org from verified JWT. */ if (request.auth) { request.context.currentUserId = request.auth.claims.sub; request.context.orgId = request.auth.claims.org_id; } return next(request);});
middleware(async (request, next) => { /** Audit all mutations. */ const result = await next(request); if (request.operationType === 'mutation') { await auditLog.record({ userId: request.context.currentUserId, operation: request.operationName, }); } return result;});Complete Schema Example
Section titled “Complete Schema Example”import { type, input, query, mutation, middleware, authenticated, requiresScope, validate } from 'fraiseql';import { ID, Email, Slug, DateTime } from 'fraiseql/scalars';
// --- Types ---
@type()class User { id: ID; username: string; email: Email; bio: string | null; createdAt: DateTime;}
@type()class Post { id: ID; title: string; slug: Slug; content: string; isPublished: boolean; createdAt: DateTime; updatedAt: DateTime; author: User; comments: Comment[];}
@type()class Comment { id: ID; content: string; createdAt: DateTime; author: User;}
// --- Inputs ---
@input()class CreatePostInput { @validate({ minLength: 1, maxLength: 200 }) title: string;
content: string; isPublished: boolean = false;}
@input()class CreateCommentInput { postId: ID;
@validate({ minLength: 1, maxLength: 10_000 }) content: string;}
// --- Queries ---
@query({ sqlSource: 'v_post' })function posts(isPublished?: boolean, limit = 20): Post[] {}
@query({ sqlSource: 'v_post', idArg: 'id' })function post(id: ID): Post | null {}
// --- Mutations ---
@mutation()@authenticated@requiresScope('write:posts')function createPost(input: CreatePostInput): Post {}
@mutation()@authenticated@requiresScope('write:comments')function createComment(input: CreateCommentInput): Comment {}
// --- Middleware ---
middleware((request, next) => { if (request.auth) { request.context.currentUserId = request.auth.claims.sub; } return next(request);});Build and Serve
Section titled “Build and Serve”-
Build the schema:
Terminal window fraiseql compileExpected output:
✓ Schema compiled: 3 types, 2 queries, 2 mutations✓ Views validated against database✓ Build complete: schema.json -
Serve the API:
Terminal window fraiseql runExpected output:
✓ FraiseQL 2.0.0 running on http://localhost:8080/graphql
Testing
Section titled “Testing”import { TestClient } from 'fraiseql/testing';
const client = new TestClient({ schemaPath: './schema.ts', databaseUrl: process.env.TEST_DB });
test('creates and fetches a post', async () => { const result = await client.mutate(` mutation { createPost(input: { title: "Hello", content: "World" }) { id title isPublished } } `);
expect(result.createPost.title).toBe('Hello'); expect(result.createPost.isPublished).toBe(false);});
test('lists published posts', async () => { const result = await client.query(` query { posts(isPublished: true, limit: 10) { id title author { username } } } `);
expect(Array.isArray(result.posts)).toBe(true);});Next Steps
Section titled “Next Steps”Custom Queries
Security
Other SDKs