Data API Builder: REST and GraphQL from SQL
Turn your SQL database into REST and GraphQL endpoints with Data API Builder β configure entities, pagination, caching, stored procedures, and deploy to Azure.
APIs without writing API code
Think of a restaurant menu.
You have a kitchen full of ingredients (your database). Customers want specific dishes (data). Normally, you hire a chef (backend developer) to take orders, cook, and serve. That is expensive and slow.
Data API Builder (DAB) is like an automated kitchen. You write a menu (configuration file) that says βthe Customers table is available as an endpoint.β DAB handles the cooking β it translates HTTP requests into SQL queries, serves the results as JSON, and handles pagination, filtering, and caching. No chef required.
You get both REST (simple, URL-based) and GraphQL (flexible, query-based) endpoints from the same config.
The DAB configuration file
Everything in DAB is driven by a single JSON configuration file. Here is the structure Leo Torres at SearchWave uses:
// dab-config.json (simplified structure)
// data-source: where the database lives
// runtime: REST and GraphQL global settings
// entities: each table/view/procedure exposed as an API
Initialising a config
# Install DAB CLI
dotnet tool install -g Microsoft.DataApiBuilder
# Initialise a new config for Azure SQL
dab init \
--database-type "mssql" \
--connection-string "@env('SQL_CONNECTION_STRING')" \
--host-mode "Development"
This creates dab-config.json with the database connection and default settings.
Adding entities
# Expose the Products table as REST and GraphQL
dab add Product \
--source "dbo.Products" \
--permissions "anonymous:read" \
--rest true \
--graphql true
# Expose a stored procedure
dab add SearchProducts \
--source "dbo.usp_SearchProducts" \
--source.type "stored-procedure" \
--source.params "searchTerm:string" \
--permissions "authenticated:execute" \
--rest.methods "get" \
--graphql.operation "query"
The resulting entity configuration in JSON:
"Product": {
"source": {
"object": "dbo.Products",
"type": "table"
},
"rest": {
"enabled": true,
"path": "/products"
},
"graphql": {
"enabled": true,
"type": {
"singular": "Product",
"plural": "Products"
}
},
"permissions": [
{
"role": "anonymous",
"actions": [
{
"action": "read"
}
]
},
{
"role": "authenticated",
"actions": [
{
"action": "*",
"fields": {
"include": ["*"],
"exclude": ["InternalCost"]
}
}
]
}
]
}
REST vs GraphQL in DAB
| Feature | REST | GraphQL |
|---|---|---|
| URL pattern | /api/products/42 | /graphql (single endpoint, query in body) |
| Field selection | Returns all fields (or use $select OData) | Client specifies exact fields needed |
| Related data | Separate requests per entity | Nested queries fetch related entities in one call |
| Mutations | POST, PUT, PATCH, DELETE verbs | mutation keyword in query body |
| Stored procedures | Mapped to specific HTTP methods | Mapped as query or mutation operations |
| Caching | HTTP cache headers (standard) | DAB-specific caching per entity |
| Best for | Simple CRUD, broad tooling support | Complex queries, mobile apps (minimise data transfer) |
REST examples
GET /api/products β List all products (paginated)
GET /api/products/42 β Get product with ID 42
POST /api/products β Create a new product (body = JSON)
PUT /api/products/42 β Replace product 42
PATCH /api/products/42 β Update specific fields
DELETE /api/products/42 β Delete product 42
# Filtering (OData syntax)
GET /api/products?$filter=price gt 100 and category eq 'Electronics'
# Sorting
GET /api/products?$orderby=price desc
# Pagination
GET /api/products?$first=10&$after=eyJpZCI6MTB9
GraphQL examples
# Query with field selection
query {
products(filter: { price: { gt: 100 } }) {
items {
id
name
price
}
}
}
# Query with nested relationship
query {
products {
items {
name
category {
name
}
reviews {
items {
rating
comment
}
}
}
}
}
# Mutation (create)
mutation {
createProduct(item: {
name: "Wireless Headphones"
price: 79.99
categoryId: 3
}) {
id
name
}
}
Pagination, filtering, and caching
Pagination
DAB uses cursor-based pagination by default (not page numbers). Each response includes nextLink (REST) or hasNextPage with endCursor (GraphQL).
// REST response with pagination
{
"value": [ ... ],
"nextLink": "/api/products?$after=eyJpZCI6MjV9"
}
Why cursor-based? It is stable when data changes. Page-number pagination breaks if rows are inserted between page requests (you might skip or duplicate items).
Caching
Enable caching to reduce database load for read-heavy endpoints:
"Product": {
"source": { "object": "dbo.Products", "type": "table" },
"cache": {
"enabled": true,
"ttl-seconds": 300
}
}
Caching works for read operations only (GET/query). Mutations always hit the database. You can set TTL (time-to-live) per entity β long TTL for rarely-changing reference data, short TTL for frequently-updated records.
Exam tip: When NOT to cache
Do not enable caching for entities where users expect real-time data: financial transactions, inventory counts, or order statuses. Stale cached data in these scenarios causes business errors. The exam may describe a scenario where a team enables caching and users see outdated stock levels β the answer is to disable caching for that entity or reduce the TTL.
Exposing stored procedures and views
Stored procedures
Stored procedures map to specific REST methods and GraphQL operation types:
"SearchProducts": {
"source": {
"object": "dbo.usp_SearchProducts",
"type": "stored-procedure",
"parameters": {
"searchTerm": "string",
"maxResults": "int"
}
},
"rest": {
"path": "/search-products",
"methods": ["get"]
},
"graphql": {
"operation": "query",
"type": {
"singular": "SearchResult",
"plural": "SearchResults"
}
}
}
Calling the procedure:
REST: GET /api/search-products?searchTerm=wireless&maxResults=10
GraphQL: query { searchProducts(searchTerm: "wireless", maxResults: 10) { ... } }
Views
Views are configured identically to tables but are read-only by default. DAB recognises that views may not support INSERT/UPDATE:
"ActiveProducts": {
"source": {
"object": "dbo.vw_ActiveProducts",
"type": "view",
"key-fields": ["id"]
}
}
The key-fields property is required for views because DAB cannot infer the primary key from the view definition.
GraphQL relationships
DAB can create GraphQL relationships between entities, allowing nested queries:
"Product": {
"source": { "object": "dbo.Products", "type": "table" },
"relationships": {
"category": {
"cardinality": "one",
"target.entity": "Category",
"source.fields": ["categoryId"],
"target.fields": ["id"]
},
"reviews": {
"cardinality": "many",
"target.entity": "Review",
"source.fields": ["id"],
"target.fields": ["productId"]
}
}
}
With these relationships, a single GraphQL query can fetch a product, its category, and all its reviews in one request. REST requires three separate calls.
Scenario: Leo builds the SearchWave product API
Leo Torres at SearchWave needs to expose their product catalog, categories, and reviews as APIs for a new mobile app. Instead of building a .NET API from scratch (weeks of work), Leo configures DAB:
- Three entities: Product, Category, Review
- GraphQL relationships so the mobile app can fetch a product with category and reviews in one call
- Caching on Category (changes rarely) and Product (5-minute TTL), no caching on Review (users expect immediate visibility)
- A stored procedure for full-text search, exposed as a GraphQL query
- Anonymous users can read products; authenticated users can create reviews
The entire API is running in under an hour. No backend code written.
Deployment options
| Option | How It Works | Best For |
|---|---|---|
| Azure Static Web Apps | Built-in database connection feature β DAB runs alongside your frontend | JAMstack apps, SPAs with database backends |
| Azure Container Apps | Run DAB as a container with auto-scaling | Production APIs needing scale and isolation |
| Azure App Service | Deploy as a container to App Service | Teams already using App Service |
| Local / self-hosted | Run dab start directly on any machine | Development and testing |
Container deployment
FROM mcr.microsoft.com/dotnet/aspnet:8.0
COPY dab-config.json /App/dab-config.json
ENV DATABASE_CONNECTION_STRING="Server=..."
ENTRYPOINT ["dab", "start", "--config", "/App/dab-config.json"]
Leo at SearchWave configures a DAB entity for the Reviews table. Users report that new reviews they submit do not appear immediately when they refresh the product page. Reviews appear after a few minutes. What is the most likely cause?
A team wants to expose a stored procedure that calculates monthly revenue. The procedure accepts a month parameter and returns aggregated sales data. How should this be configured in DAB?
π¬ Video coming soon
Next up: Securing AI and API Endpoints β lock down your REST, GraphQL, and AI model endpoints with authentication, authorization, and managed identity.