Data Service CRUD Operations
Complete guide to Creating, Reading, Updating, and Deleting data in Taruvi Data Service tables.
Overview
The Data Service provides a complete CRUD (Create, Read, Update, Delete) API for manipulating data in your tables. These operations work on the actual data stored in materialized tables, as opposed to schema operations which manage table structures.
Base URL: /api/apps/{app_slug}/datatables/{name}/data/
Key Features:
- Single and bulk operations
- Upsert capabilities (insert or update)
- Advanced querying with filters, sorting, pagination
- Relationship population
- Soft delete support
- Automatic validation
Creating Data
Single Record Creation
Create a single record by sending a JSON object:
POST /api/apps/blog/datatables/posts/data/
Content-Type: application/json
{
"title": "My First Post",
"content": "This is the content of my post",
"author_id": 1,
"status": "draft"
}
Response (201 Created):
{
"data": {
"id": 42,
"title": "My First Post",
"content": "This is the content of my post",
"author_id": 1,
"status": "draft",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
}
Bulk Insert
Create multiple records at once by sending an array:
POST /api/apps/blog/datatables/comments/data/
Content-Type: application/json
[
{
"post_id": 42,
"author": "Alice",
"content": "Great post!"
},
{
"post_id": 42,
"author": "Bob",
"content": "Thanks for sharing"
}
]
Response (201 Created):
{
"data": [
{
"id": 1,
"post_id": 42,
"author": "Alice",
"content": "Great post!",
"created_at": "2024-01-15T10:31:00Z"
},
{
"id": 2,
"post_id": 42,
"author": "Bob",
"content": "Thanks for sharing",
"created_at": "2024-01-15T10:31:00Z"
}
]
}
Upsert (Insert or Update)
Update existing records or create new ones based on unique fields:
POST /api/apps/blog/datatables/users/data/upsert/?unique_fields=email
Content-Type: application/json
{
"email": "alice@example.com",
"name": "Alice Smith",
"bio": "Software engineer"
}
If email exists - Updates the existing record If email doesn't exist - Creates a new record
Response (200 OK or 201 Created):
{
"data": {
"id": 5,
"email": "alice@example.com",
"name": "Alice Smith",
"bio": "Software engineer",
"created_at": "2024-01-10T08:00:00Z",
"updated_at": "2024-01-15T10:32:00Z"
},
"created": false
}
Bulk Upsert:
POST /api/apps/blog/datatables/users/data/upsert/?unique_fields=email
Content-Type: application/json
[
{"email": "alice@example.com", "name": "Alice"},
{"email": "bob@example.com", "name": "Bob"}
]
Validation
All create operations validate data against the table schema:
- Required fields: Must be present
- Type checking: Values must match field types (string, integer, etc.)
- Constraints: Min/max length, pattern matching, enum values
- Foreign keys: Referenced IDs must exist
- Unique constraints: Duplicate values rejected
Validation Error Example (400 Bad Request):
{
"error": "Validation failed",
"details": {
"title": ["This field is required"],
"author_id": ["Foreign key constraint violated: author with id=999 does not exist"],
"status": ["Value must be one of: draft, published, archived"]
}
}
Reading Data
List All Records
Fetch all records with pagination:
GET /api/apps/blog/datatables/posts/data/
Response (200 OK):
{
"data": [
{
"id": 1,
"title": "First Post",
"author_id": 1,
"created_at": "2024-01-01T12:00:00Z"
},
{
"id": 2,
"title": "Second Post",
"author_id": 2,
"created_at": "2024-01-02T12:00:00Z"
}
],
"total": 50,
"page": 1,
"page_size": 20
}
Get Single Record
Fetch a specific record by ID:
GET /api/apps/blog/datatables/posts/data/42/
Response (200 OK):
{
"data": {
"id": 42,
"title": "My First Post",
"content": "This is the content",
"author_id": 1,
"status": "published",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T11:00:00Z"
}
}
Not Found (404):
{
"error": "Record not found",
"detail": "No post with id=999"
}
Pagination
Page-based pagination:
GET /api/apps/blog/datatables/posts/data/?page=2&page_size=10
Offset-based pagination:
GET /api/apps/blog/datatables/posts/data/?limit=10&offset=20
Filtering
Apply filters using query parameters. See Querying Guide for complete filter operators.
GET /api/apps/blog/datatables/posts/data/?status=published&author_id=1
GET /api/apps/blog/datatables/posts/data/?created_at__gte=2024-01-01&title__contains=tutorial
Sorting
Sort by one or more fields:
GET /api/apps/blog/datatables/posts/data/?ordering=-created_at,title
- Ascending:
ordering=title - Descending:
ordering=-created_at(prefix with -) - Multiple:
ordering=-created_at,title
Populate Relationships
Load related data in a single query:
GET /api/apps/blog/datatables/posts/data/?populate=author,comments
Response with populated relationships:
{
"data": [
{
"id": 1,
"title": "My Post",
"author_id": 5,
"author": {
"id": 5,
"name": "Alice",
"email": "alice@example.com"
},
"comments": [
{"id": 1, "content": "Great!", "author": "Bob"},
{"id": 2, "content": "Thanks", "author": "Carol"}
]
}
]
}
Populate all relationships:
GET /api/apps/blog/datatables/posts/data/?populate=*
See Relationships Guide for advanced population.
Response Formats
Flat Format (default):
GET /api/apps/blog/datatables/posts/data/
Tree Format (hierarchical data):
GET /api/apps/blog/datatables/categories/data/?format=tree
Graph Format (nodes and edges):
GET /api/apps/blog/datatables/users/data/?format=graph&include=descendants
See Hierarchy Guide for tree/graph formats.
Updating Data
Update Single Record
Partially update a record (only specified fields):
PATCH /api/apps/blog/datatables/posts/data/42/
Content-Type: application/json
{
"status": "published",
"published_at": "2024-01-15T12:00:00Z"
}
Response (200 OK):
{
"data": {
"id": 42,
"title": "My First Post",
"content": "This is the content",
"status": "published",
"published_at": "2024-01-15T12:00:00Z",
"updated_at": "2024-01-15T12:00:00Z"
}
}
Bulk Update
Update multiple records at once. Each object must include id:
PATCH /api/apps/blog/datatables/posts/data/
Content-Type: application/json
[
{
"id": 42,
"status": "published"
},
{
"id": 43,
"status": "archived"
}
]
Response (200 OK):
{
"data": [
{"id": 42, "status": "published", "updated_at": "2024-01-15T12:00:00Z"},
{"id": 43, "status": "archived", "updated_at": "2024-01-15T12:00:00Z"}
]
}
Validation on Update
Updates are validated against the schema:
PATCH /api/apps/blog/datatables/posts/data/42/
Content-Type: application/json
{
"status": "invalid_status"
}
Response (400 Bad Request):
{
"error": "Validation failed",
"details": {
"status": ["Value must be one of: draft, published, archived"]
}
}
Deleting Data
Delete Single Record
DELETE /api/apps/blog/datatables/posts/data/42/
Response (204 No Content)
Bulk Delete by IDs
Delete multiple records by specifying IDs:
DELETE /api/apps/blog/datatables/posts/data/?ids=42,43,44
Response (200 OK):
{
"deleted": 3,
"ids": [42, 43, 44]
}
Bulk Delete by Filter
Delete all records matching a filter:
DELETE /api/apps/blog/datatables/posts/data/?filter={"status":"draft","created_at__lt":"2023-01-01"}
Response (200 OK):
{
"deleted": 15
}
Soft Delete
If your table has an is_deleted field, deletions are soft by default:
DELETE /api/apps/blog/datatables/posts/data/42/
The record is marked as deleted (is_deleted=true) but remains in the database.
Querying with soft delete:
- By default, soft-deleted records are excluded from queries
- Include deleted:
GET /data/?include_deleted=true - Only deleted:
GET /data/?only_deleted=true
Hard Delete
Permanently remove records:
DELETE /api/apps/blog/datatables/posts/data/42/?hard_delete=true
Cascade Behavior
Foreign key cascade rules are respected:
- CASCADE: Related records are deleted
- SET NULL: Foreign keys are set to NULL
- RESTRICT: Delete fails if related records exist
Example (403 Forbidden):
{
"error": "Cannot delete record",
"detail": "Post has 5 related comments. Delete comments first or use CASCADE."
}
Error Handling
Common HTTP Status Codes
| Code | Meaning | Example |
|---|---|---|
| 200 | Success | Record updated |
| 201 | Created | New record created |
| 204 | No Content | Record deleted |
| 400 | Bad Request | Validation failed |
| 404 | Not Found | Record doesn't exist |
| 409 | Conflict | Unique constraint violation |
| 422 | Unprocessable Entity | Business logic error |
Validation Errors (400)
{
"error": "Validation failed",
"details": {
"email": ["This field is required"],
"age": ["Value must be >= 0"]
}
}
Unique Constraint Violations (409)
{
"error": "Unique constraint violated",
"detail": "Record with email='alice@example.com' already exists",
"field": "email"
}
Foreign Key Violations (400)
{
"error": "Foreign key constraint violated",
"detail": "Author with id=999 does not exist",
"field": "author_id"
}
Best Practices
1. Use Bulk Operations for Multiple Records
❌ Don't:
// Inefficient: 100 separate requests
for (const user of users) {
await fetch('/data/', {
method: 'POST',
body: JSON.stringify(user)
});
}
✅ Do:
// Efficient: Single bulk request
await fetch('/data/', {
method: 'POST',
body: JSON.stringify(users) // Array of objects
});
2. Use Upsert for Idempotency
When you're unsure if a record exists, use upsert:
// Idempotent operation
await fetch('/data/upsert/?unique_fields=email', {
method: 'POST',
body: JSON.stringify({
email: 'alice@example.com',
name: 'Alice'
})
});
3. Populate Only Needed Relationships
❌ Don't:
GET /data/?populate=* # Loads ALL relationships (slow!)
✅ Do:
GET /data/?populate=author # Only what you need
4. Paginate Large Result Sets
Always use pagination for tables with many records:
GET /data/?page_size=50&page=1
5. Use Partial Updates
// Only send changed fields
await fetch('/data/42/', {
method: 'PATCH',
body: JSON.stringify({ status: 'published' }) // Not the entire record
});
6. Handle Errors Gracefully
try {
const response = await fetch('/data/', {
method: 'POST',
body: JSON.stringify(newRecord)
});
if (!response.ok) {
const error = await response.json();
if (response.status === 400) {
// Handle validation errors
console.error('Validation errors:', error.details);
} else if (response.status === 409) {
// Handle unique constraint violation
console.error('Duplicate record:', error.detail);
}
}
const data = await response.json();
return data;
} catch (error) {
console.error('Network error:', error);
}
7. Leverage Transactions (Single vs Bulk)
- Single operations: Atomic by default
- Bulk operations: All-or-nothing transaction
- If any record fails validation, entire bulk operation is rolled back
Examples
Blog Post Creation
// Create a new blog post
const newPost = await fetch('/api/apps/blog/datatables/posts/data/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify({
title: 'Getting Started with Taruvi',
content: 'Taruvi is a powerful backend platform...',
author_id: 5,
category: 'tutorial',
status: 'draft',
tags: ['taruvi', 'tutorial', 'backend']
})
}).then(r => r.json());
console.log('Created post:', newPost.data);
User Profile Update
import requests
# Update user profile
response = requests.patch(
'https://api.example.com/api/apps/myapp/datatables/users/data/42/',
headers={'Authorization': 'Bearer YOUR_TOKEN'},
json={
'bio': 'Software Engineer at Acme Corp',
'location': 'San Francisco, CA',
'website': 'https://example.com'
}
)
if response.status_code == 200:
user = response.json()['data']
print(f"Updated user: {user['name']}")
Bulk Import CSV Data
// Convert CSV to JSON and bulk insert
const csvData = `name,email,age
Alice,alice@example.com,30
Bob,bob@example.com,25
Carol,carol@example.com,35`;
const users = csvData.split('\n').slice(1).map(line => {
const [name, email, age] = line.split(',');
return { name, email, age: parseInt(age) };
});
const response = await fetch('/api/apps/myapp/datatables/users/data/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN'
},
body: JSON.stringify(users)
});
const result = await response.json();
console.log(`Imported ${result.data.length} users`);
Archive Old Records
import requests
from datetime import datetime, timedelta
# Archive posts older than 1 year
one_year_ago = (datetime.now() - timedelta(days=365)).isoformat()
response = requests.patch(
'https://api.example.com/api/apps/blog/datatables/posts/data/',
headers={'Authorization': 'Bearer YOUR_TOKEN'},
params={
'created_at__lt': one_year_ago,
'status': 'published'
},
json={'status': 'archived'}
)
print(f"Archived {response.json()['total']} posts")
Related Documentation
- Querying Guide - Complete filter operators and query syntax
- Aggregations - GROUP BY and aggregate functions
- Relationships - Working with foreign keys and populate
- Schema Management - Defining table structures
- API Endpoints Reference - Complete endpoint listing