Skip to main content

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.

UUID Primary Keys

Tables with a single primary key automatically use UUID format. You can omit the id field when creating records — a UUID will be generated automatically.

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):

{
"status": "success",
"message": "Record created successfully",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"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

You can also filter by fields on a related (foreign-keyed) record using dot notation in the filter key — see Filtering on Related Records. The related row is not added to the response unless you also pass populate:

# Posts whose author's display name contains "alice"
GET /api/apps/blog/datatables/posts/data/?author_id.display_name__contains=alice

# Authors who have at least one published post (reverse FK)
GET /api/apps/blog/datatables/users/data/?posts.status=published

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 delete rules are respected based on foreignKeys[].x-actions.onDelete in the table schema.

  • CASCADE: Related records are deleted
  • SET NULL: Foreign keys are set to NULL
  • SET DEFAULT: Foreign keys are set to their field default
  • RESTRICT: Delete fails if related records exist
  • NO ACTION: PostgreSQL no-action behavior is used

If x-actions.onDelete is omitted, Taruvi normalizes the FK to RESTRICT.

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​

CodeMeaningExample
200SuccessRecord updated
201CreatedNew record created
204No ContentRecord deleted
400Bad RequestValidation failed
404Not FoundRecord doesn't exist
409ConflictUnique constraint violation
422Unprocessable EntityBusiness 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")