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:

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": "[email protected]",
"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": "[email protected]",
"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": "[email protected]", "name": "Alice"},
{"email": "[email protected]", "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": "[email protected]"
},
"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

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='[email protected]' 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: '[email protected]',
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,[email protected],30
Bob,[email protected],25
Carol,[email protected],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")