Public APIs for third-party developers
REST is universal. Developers can consume it with any HTTP client, without GraphQL knowledge or tooling.
This is not a generic REST vs GraphQL article. Both transports are served from the same FraiseQL binary, compiled from the same schema. The choice is per-operation, not per-project.
FraiseQL compiles your schema into a single Rust binary that serves REST and GraphQL simultaneously. You can expose some operations only via GraphQL, others via REST and GraphQL, and others via all three transports (REST, GraphQL, gRPC).
Auth, rate limiting, and RBAC are shared across transports. There is no separate REST service to run or maintain.
Public APIs for third-party developers
REST is universal. Developers can consume it with any HTTP client, without GraphQL knowledge or tooling.
CDN caching
GET endpoints are cacheable by default. GraphQL POST requests are not, without extra infrastructure.
OpenAPI tooling
The OpenAPI 3.0.3 spec is auto-generated from your schema at compile time — no manual YAML, works with any OpenAPI-compatible tool.
Mobile clients
HTTP/1.1 compatible, simpler caching model, no GraphQL client library required.
Third-party integrations
Webhooks, SaaS integrations, and automation tools expect REST. The auto-generated spec lets them discover your API without documentation.
Teams already using OpenAPI
If your team generates SDKs from OpenAPI specs or uses tools like Swagger UI, the auto-generated spec slots in without process changes.
Complex nested data needs
Field selection avoids over-fetching. Clients request exactly the fields they need, reducing payload size.
Multiple clients, different requirements
One GraphQL endpoint serves mobile, web, and internal tools — each with a different query shape.
Real-time with subscriptions
Native GraphQL subscription protocol (graphql-transport-ws and graphql-ws). REST has no equivalent.
Rapid iteration
Add fields to a type without versioning the API. Existing clients are unaffected.
Add rest_path and rest_method annotations to expose any operation via REST alongside GraphQL.
import fraiseqlfrom uuid import UUID
@fraiseql.typeclass Post: id: UUID title: str body: str published_at: str
@fraiseql.query( sql_source="v_post", rest_path="/posts", # REST: GET /rest/posts rest_method="GET",)def posts(limit: int = 10) -> list[Post]: ...REST clients hit GET /rest/posts. GraphQL clients send query { posts { ... } }. Both execute against the same SQL view v_post. Auth, rate limiting, and RBAC are shared.
# RESTcurl https://api.example.com/rest/posts?limit=5 \ -H "Authorization: Bearer $TOKEN"
# GraphQL (same data)curl -X POST https://api.example.com/graphql \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"query": "{ posts(limit: 5) { id title } }"}'The REST response uses a {"data": [...], "meta": {...}, "links": {...}} envelope. The GraphQL response wraps the data in {"data": {"posts": [...]}}. Both are backed by the same compiled SQL view.
If you already have a GraphQL schema, adding REST annotations is incremental. You do not need to restructure your schema or recompile everything from scratch.
rest_path and rest_method to the operations you want to expose via REST.fraiseql compile.fraiseql run.Existing GraphQL clients are unaffected. REST annotations only add new routes — they do not change GraphQL behavior.
The reverse works too: if you have REST annotations and want to add GraphQL later, existing REST routes are unaffected when GraphQL is enabled.
import fraiseqlfrom uuid import UUID
@fraiseql.typeclass Post: id: UUID title: str body: str author_id: UUID published_at: str
# List posts@fraiseql.query( sql_source="v_post", rest_path="/posts", rest_method="GET",)def posts(limit: int = 10, offset: int = 0) -> list[Post]: ...
# Get post by ID@fraiseql.query( sql_source="v_post", rest_path="/posts/{id}", rest_method="GET",)def post(id: UUID) -> Post: ...
# Create post (mutation)@fraiseql.mutation( sql_source="fn_create_post", rest_path="/posts", rest_method="POST",)def create_post(title: str, body: str) -> Post: ...# List postscurl https://api.example.com/rest/posts?limit=10 \ -H "Authorization: Bearer $TOKEN"
# Get post by IDcurl https://api.example.com/rest/posts/a1b2c3d4-... \ -H "Authorization: Bearer $TOKEN"
# Create postcurl -X POST https://api.example.com/rest/posts \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"title": "Hello", "body": "World"}'# List postsquery { posts(limit: 10) { id title publishedAt }}
# Get post by IDquery { post(id: "a1b2c3d4-...") { id title body authorId }}
# Create postmutation { createPost(title: "Hello", body: "World") { id title }}All six operations above resolve against the same compiled schema. There is one binary to deploy and one fraiseql.toml to configure.
See REST Transport for the full annotation reference, OpenAPI spec generation, and configuration options.