Build Your First API
Get a working GraphQL API in minutes. Quick Start Guide
Both FraiseQL and Hasura provide GraphQL APIs over databases. Here’s how they differ.
Hasura is a runtime GraphQL engine that interprets queries and generates SQL on-the-fly.
FraiseQL is a compiled Rust binary that pre-generates optimized SQL views at build time — no runtime SQL generation, no resolver code.
| Aspect | FraiseQL | Hasura |
|---|---|---|
| Architecture | Compiled Rust binary | Interpreted runtime engine |
| Query execution | Pre-built SQL views | Runtime SQL generation |
| N+1 handling | Eliminated by design | Runtime batching |
| Configuration | TOML | Console + YAML |
| Schema source | Code (Python, TS, Go…) | Database introspection |
| Database support | PostgreSQL, MySQL, SQLite, SQL Server | PostgreSQL (primary), others via connectors |
| Deployment | Single binary | Docker container + metadata |
| Performance | Predictable, sub-ms | Variable, depends on query |
| Custom logic | Observers (reactive events) | Actions (HTTP), Remote Schemas |
| Pricing | Open source (Apache 2.0) | Open source core, paid cloud |
Pricing information accurate as of February 2026. Verify current pricing at hasura.io/pricing.
If you know Hasura, here is how its concepts map to FraiseQL equivalents:
| Hasura | FraiseQL Equivalent |
|---|---|
| Track table | Create SQL view (v_*) |
| Permissions (YAML) | PostgreSQL Row-Level Security |
| Event triggers | Observers |
| Actions | SQL functions + mutations |
| Remote schemas | Federation |
| Computed fields | SQL view expressions |
-- Composed views: tb_ tables, v_ views, child .data embedded in parentCREATE VIEW v_user ASSELECT id, jsonb_build_object('id', id, 'name', name, 'email', email) AS dataFROM tb_user;
CREATE VIEW v_post ASSELECT p.id, jsonb_build_object('id', p.id, 'title', p.title, 'author', vu.data) AS dataFROM tb_post pJOIN v_user vu ON vu.id = p.fk_user;Query execution: Single indexed view lookup
-- Generated at runtime per querySELECT u.* FROM users u WHERE u.id = $1;SELECT p.* FROM posts p WHERE p.author_id IN ($1, $2, ...);-- Results assembled in memoryQuery execution: Multiple queries + assembly
[project]name = "my-api"
[database]url = "${DATABASE_URL}"
[server]port = 8080# JWT authentication via env var (no [auth] section in fraiseql.toml)JWT_SECRET=your-256-bit-secretversion: 3metadata_directory: metadataactions: handler_webhook_baseurl: http://localhost:3000Plus separate files for:
tables.yamlrelationships.yamlpermissions.yamlremote_schemas.yaml@fraiseql.typeclass User: """User with posts.""" id: str name: str email: str posts: list['Post']import { type } from 'fraiseql';
@type()class User { id!: string; name!: string; email!: string; posts!: Post[];}import "github.com/fraiseql/fraiseql-go"
type User struct { ID fraiseql.ID `fraiseql:"id"` Name string `fraiseql:"name"` Email string `fraiseql:"email"` Posts []Post `fraiseql:"posts"`}FraiseQL uses observers for reactive business logic — you define conditions and actions that trigger on database changes:
from fraiseql import observer, webhook, slack, emailfrom fraiseql.observers import RetryConfig
@observer( entity="Order", event="INSERT", condition="total > 1000", actions=[ webhook("https://api.example.com/high-value-orders"), slack("#sales", "High-value order {id}: ${total}"), email( to="sales@example.com", subject="High-value order {id}", body="Order {id} for ${total} was created", ), ], retry=RetryConfig(max_attempts=5, backoff_strategy="exponential"),)def on_high_value_order(): """Triggered when a high-value order is created.""" passimport { observer, webhook, slack, email } from 'fraiseql';
observer({ entity: 'Order', event: 'INSERT', condition: 'total > 1000', actions: [ webhook('https://api.example.com/high-value-orders'), slack('#sales', 'High-value order {id}: ${total}'), email({ to: 'sales@example.com', subject: 'High-value order {id}', body: 'Order {id} for ${total} was created', }), ], retry: { maxAttempts: 5, backoffStrategy: 'exponential' },});Built-in actions for webhooks, Slack, and email. No external services required.
- name: processOrder definition: kind: synchronous handler: http://business-logic-service:3000/process-orderPlus a separate HTTP service to handle the logic. Hasura also has event triggers, but they require external webhook handlers.
- name: on_order_created definition: enable_manual: false insert: columns: "*" retry_conf: num_retries: 3 interval_sec: 30 timeout_sec: 60 webhook: https://my-service.example.com/on-order-created headers: - name: X-Webhook-Secret value_from_env: WEBHOOK_SECRETHasura calls your external HTTP webhook. You own and deploy a separate service to handle the event.
@observer Decoratorfrom fraiseql import observer, webhook, slack, emailfrom fraiseql.observers import RetryConfig
@observer( entity="Order", event="INSERT", actions=[ webhook("https://my-service.example.com/on-order-created", headers={"X-Webhook-Secret": "{env.WEBHOOK_SECRET}"}), slack("#orders", "New order {id} received"), ], retry=RetryConfig(max_attempts=3, interval_sec=30, timeout_sec=60),)def on_order_created(): """Triggered when a new order is inserted.""" passimport { observer, webhook, slack } from 'fraiseql';
observer({ entity: 'Order', event: 'INSERT', actions: [ webhook('https://my-service.example.com/on-order-created', { headers: { 'X-Webhook-Secret': '{env.WEBHOOK_SECRET}' }, }), slack('#orders', 'New order {id} received'), ], retry: { maxAttempts: 3, intervalSec: 30, timeoutSec: 60 },});import "github.com/fraiseql/fraiseql-go"
func init() { fraiseql.Observer(fraiseql.ObserverConfig{ Entity: "Order", Event: "INSERT", Actions: []fraiseql.Action{ fraiseql.Webhook("https://my-service.example.com/on-order-created", fraiseql.WithHeader("X-Webhook-Secret", "{env.WEBHOOK_SECRET}")), fraiseql.Slack("#orders", "New order {id} received"), }, Retry: fraiseql.RetryConfig{MaxAttempts: 3, IntervalSec: 30, TimeoutSec: 60}, })}The key difference: Hasura event triggers require you to host an external HTTP service. FraiseQL observers are declared inline in your schema code — actions like webhook, slack, and email are built-in, with no separate service required.
FraiseQL enforces validation during schema compilation, before any query executes:
All validation is declarative — rules are defined via decorators in your schema code:
@fraiseql.typeclass CreateUserInput: email: Annotated[str, fraiseql.field(pattern=r"^[^@]+@[^@]+\.[^@]+$")] age: Annotated[int, fraiseql.field(range={"min": 0, "max": 150})] phone: Annotated[str, fraiseql.field(length=10)]Hasura relies primarily on GraphQL’s built-in type system for validation:
For anything beyond GraphQL type checking, Hasura users must implement custom Actions (external HTTP services).
| Aspect | FraiseQL | Hasura |
|---|---|---|
| Built-in validators | 13 rules | ~3 (via GraphQL types) |
| Compile-time enforcement | ✅ Yes | ❌ No |
| Mutual exclusivity | OneOf, AnyOf, ConditionalRequired, RequiredIfAbsent | @oneOf only |
| Cross-field validation | ✅ Yes | ❌ Custom Actions required |
| Database protection | Invalid data impossible | Possible without Actions |
| Configuration | Declarative TOML | GraphQL directives + Actions |
Hasura is a better choice when:
FraiseQL is a better choice when:
Start Hasura locally:
docker run -d --name hasura \ -p 8080:8080 \ -e HASURA_GRAPHQL_DATABASE_URL=postgresql://user:pass@host/db \ hasura/graphql-engine:latestOpen the console at http://localhost:8080/console
Manual steps required:
Create the same API with FraiseQL:
# fraiseql.toml - single file[database]url = "${DATABASE_URL}"# schema.py - code-firstimport fraiseql
@fraiseql.typeclass User: id: str name: str email: str posts: list['Post']
@fraiseql.typeclass Post: id: str title: str author: Userfraiseql runNo manual configuration — schema derived from code automatically.
Compare query performance:
Hasura generates SQL at runtime:
-- Generated per requestSELECT * FROM users WHERE ...;SELECT * FROM posts WHERE user_id IN (...);FraiseQL uses pre-built views:
-- Single view querySELECT data FROM v_user WHERE id = $1;Test GraphQL introspection:
# Both expose same introspectioncurl -X POST http://localhost:8080/v1/graphql \ -H "Content-Type: application/json" \ -d '{"query": "{ __schema { types { name } } }"}'
curl -X POST http://localhost:8080/graphql \ -H "Content-Type: application/json" \ -d '{"query": "{ __schema { types { name } } }"}'# Get your Hasura table definitionshasura metadata exportHasura table:
- table: name: users schema: public object_relationships: - name: posts using: foreign_key_constraint_on: column: author_id table: name: postsFraiseQL equivalent:
@fraiseql.typeclass User: id: str name: str email: str posts: list['Post']
@fraiseql.typeclass Post: id: str title: str author: Userimport { type } from 'fraiseql';
@type()class User { id!: string; name!: string; email!: string; posts!: Post[];}
@type()class Post { id!: string; title!: string; author!: User;}import "github.com/fraiseql/fraiseql-go"
type User struct { ID fraiseql.ID `fraiseql:"id"` Name string `fraiseql:"name"` Email string `fraiseql:"email"` Posts []Post `fraiseql:"posts"`}
type Post struct { ID fraiseql.ID `fraiseql:"id"` Title string `fraiseql:"title"` Author User `fraiseql:"author"`}Hasura Action:
- name: processPayment definition: kind: synchronous handler: http://payments:3000/processFraiseQL observer:
@observer( entity="Order", event="UPDATE", condition="status = 'pending_payment'", actions=[ webhook("https://payments.internal/process", body={"order_id": "{id}", "amount": "{total}"}), ],)def on_pending_payment(): """Process payment when order status changes.""" passAlready on Hasura and want to try FraiseQL? Here are the three core steps:
Install the FraiseQL CLI
# Download the FraiseQL binarycurl -fsSL https://install.fraiseql.dev | sh
# Verify installationfraiseql --versionConvert your Hasura YAML to a FraiseQL schema
Export your Hasura metadata, then rewrite each table definition as a FraiseQL type and write the corresponding SQL views (v_*) to enforce permissions:
# Export existing Hasura configurationhasura metadata export
# Inspect generated metadatals metadata/databases/default/tables/For each Hasura table, create a FraiseQL type and a SQL view. See the examples above for the conversion pattern, or refer to the full migration guide.
Run FraiseQL
# Start the FraiseQL server pointing at your existing databasefraiseql run --config fraiseql.tomlFraiseQL will read your schema types, connect to your database, and serve the GraphQL API on the configured port. You can keep Hasura running in parallel during the transition.
For a complete walkthrough including subscription migration and permission replication, see the Migrating from Hasura guide.
Migrating from Hasura? See the step-by-step migration guide.
| Choose | When |
|---|---|
| Hasura | Rapid prototyping, existing databases, GUI preference |
| FraiseQL | Predictable performance, code-first, multi-database |
Both are excellent tools. Choose based on your team’s preferences and requirements.
Build Your First API
Get a working GraphQL API in minutes. Quick Start Guide
Performance Benchmarks
See how FraiseQL performs with real numbers. View Benchmarks
Migrate from Hasura
Step-by-step guide to moving your existing Hasura setup. Migration Guide
How FraiseQL Works
Understand the compiled, database-first architecture. Core Concepts