Building a Blog API with Ductape
Learn how to build a complete blog API with Ductape, including posts, comments, user management, and file uploads.
What You'll Build
- User authentication and sessions
- CRUD operations for blog posts
- Comment system with nested replies
- File uploads for post images
- Search and filtering
- Real-time notifications
Prerequisites
- TypeScript
- Java
- Go
- .NET
npm install @ductape/sdk@0.1.8
<dependency>
<groupId>app.ductape</groupId>
<artifactId>sdk</artifactId>
<version>0.1.8</version>
</dependency>
go get github.com/ductape/ductape/sdk/go@v0.1.8
dotnet add package Ductape.Sdk --version 0.1.8
Step 1: Initialize Your Product
- TypeScript
- Java
- Go
- .NET
import { Ductape } from '@ductape/sdk';
const ductape = new Ductape({
apiKey: process.env.DUCTAPE_API_KEY!,
});
import app.ductape.sdk.Ductape;
import app.ductape.sdk.core.EnvType;
import app.ductape.sdk.core.RequestContext;
Map<String, Object> ductape = new Ductape(Map.of(
apiKey: System.getenv("DUCTAPE_API_KEY")!
));
import { Ductape } from '@ductape/sdk';
ductape := new Ductape({
apiKey: os.Getenv("DUCTAPE_API_KEY")!,
});
import { Ductape } from '@ductape/sdk';
var ductape = new Ductape({
apiKey: Environment.GetEnvironmentVariable("DUCTAPE_API_KEY")!,
});
Step 2: Set Up Database Schema
- TypeScript
- Java
- Go
- .NET
import { DatabaseType } from '@ductape/sdk';
// Create database
await ductape.databases.create({
name: 'BlogDB',
tag: 'blog-db',
type: DatabaseType.POSTGRESQL,
envs: [
{
slug: 'prd',
connection_url: process.env.DATABASE_URL!
}
]
});
// Connect to database
await ductape.databases.connect({
database: 'blog-db'
});
// Create users table
await ductape.databases.schema.create('users', {
name: { type: 'String', required: true },
email: { type: 'String', unique: true, required: true },
password: { type: 'String', required: true },
bio: { type: 'String' },
avatar_url: { type: 'String' },
role: { type: 'String', default: 'user' }, // user, editor, admin
created_at: { type: 'Date', default: 'now' },
updated_at: { type: 'Date', default: 'now' }
});
// Create posts table
await ductape.databases.schema.create('posts', {
title: { type: 'String', required: true },
slug: { type: 'String', unique: true, required: true },
content: { type: 'String', required: true },
excerpt: { type: 'String' },
featured_image: { type: 'String' },
author_id: { type: 'String', required: true },
status: { type: 'String', default: 'draft' }, // draft, published, archived
tags: { type: 'Array' },
view_count: { type: 'Number', default: 0 },
published_at: { type: 'Date' },
created_at: { type: 'Date', default: 'now' },
updated_at: { type: 'Date', default: 'now' }
});
// Create comments table
await ductape.databases.schema.create('comments', {
post_id: { type: 'String', required: true },
user_id: { type: 'String', required: true },
content: { type: 'String', required: true },
parent_id: { type: 'String' }, // For nested replies
created_at: { type: 'Date', default: 'now' },
updated_at: { type: 'Date', default: 'now' }
});
// Create indexes for better performance
await ductape.databases.schema.createIndex('posts', ['author_id']);
await ductape.databases.schema.createIndex('posts', ['slug'], { unique: true });
await ductape.databases.schema.createIndex('posts', ['status', 'published_at']);
await ductape.databases.schema.createIndex('comments', ['post_id']);
await ductape.databases.schema.createIndex('comments', ['user_id']);
import Map.of( DatabaseType ) from '@ductape/sdk';
// Create database
ductape.databases.create(Map.of(
"name", "BlogDB",
"tag", "blog-db",
type: DatabaseType.POSTGRESQL,
envs: [
Map.of(
"slug", "prd",
connection_url: System.getenv("DATABASE_URL")!
)
]
));
// Connect to database
ductape.databases().connect(Map<String, Object>.of(
"database", "blog-db"
));
// Create users table
ductape.databases.schema.create('users', Map.of(
name: Map.of( "type", "String", "required", true ),
email: Map.of( "type", "String", "unique", true, "required", true ),
password: Map.of( "type", "String", "required", true ),
bio: Map.of( "type", "String" ),
avatar_url: Map.of( "type", "String" ),
role: Map.of( "type", "String", "default", "user" ), // user, editor, admin
created_at: Map.of( "type", "Date", "default", "now" ),
updated_at: Map.of( "type", "Date", "default", "now" )
));
// Create posts table
ductape.databases.schema.create('posts', Map.of(
title: Map.of( "type", "String", "required", true ),
slug: Map.of( "type", "String", "unique", true, "required", true ),
content: Map.of( "type", "String", "required", true ),
excerpt: Map.of( "type", "String" ),
featured_image: Map.of( "type", "String" ),
author_id: Map.of( "type", "String", "required", true ),
status: Map.of( "type", "String", "default", "draft" ), // draft, published, archived
tags: Map.of( "type", "Array" ),
view_count: Map.of( "type", "Number", "default", 0 ),
published_at: Map.of( "type", "Date" ),
created_at: Map.of( "type", "Date", "default", "now" ),
updated_at: Map.of( "type", "Date", "default", "now" )
));
// Create comments table
ductape.databases.schema.create('comments', Map.of(
post_id: Map.of( "type", "String", "required", true ),
user_id: Map.of( "type", "String", "required", true ),
content: Map.of( "type", "String", "required", true ),
parent_id: Map.of( "type", "String" ), // For nested replies
created_at: Map.of( "type", "Date", "default", "now" ),
updated_at: Map.of( "type", "Date", "default", "now" )
));
// Create indexes for better performance
ductape.databases.schema.createIndex('posts', ['author_id']);
ductape.databases.schema.createIndex('posts', ['slug'], Map.of( "unique", true ));
ductape.databases.schema.createIndex('posts', ['status', 'published_at']);
ductape.databases.schema.createIndex('comments', ['post_id']);
ductape.databases.schema.createIndex('comments', ['user_id']);
import "context"
import { DatabaseType } from '@ductape/sdk';
// Create database
client.databases.create({
"name": "BlogDB",
"tag": "blog-db",
type: DatabaseType.POSTGRESQL,
envs: [
{
"slug": "prd",
connection_url: os.Getenv("DATABASE_URL")!
}
]
});
// Connect to database
client.Databases.Connect(ctx, map[string]any{
"database": "blog-db"
});
// Create users table
client.databases.schema.create('users', {
name: { "type": "String", "required": true },
email: { "type": "String", "unique": true, "required": true },
password: { "type": "String", "required": true },
bio: { "type": "String" },
avatar_url: { "type": "String" },
role: { "type": "String", "default": "user" }, // user, editor, admin
created_at: { "type": "Date", "default": "now" },
updated_at: { "type": "Date", "default": "now" }
});
// Create posts table
client.databases.schema.create('posts', {
title: { "type": "String", "required": true },
slug: { "type": "String", "unique": true, "required": true },
content: { "type": "String", "required": true },
excerpt: { "type": "String" },
featured_image: { "type": "String" },
author_id: { "type": "String", "required": true },
status: { "type": "String", "default": "draft" }, // draft, published, archived
tags: { "type": "Array" },
view_count: { "type": "Number", "default": 0 },
published_at: { "type": "Date" },
created_at: { "type": "Date", "default": "now" },
updated_at: { "type": "Date", "default": "now" }
});
// Create comments table
client.databases.schema.create('comments', {
post_id: { "type": "String", "required": true },
user_id: { "type": "String", "required": true },
content: { "type": "String", "required": true },
parent_id: { "type": "String" }, // For nested replies
created_at: { "type": "Date", "default": "now" },
updated_at: { "type": "Date", "default": "now" }
});
// Create indexes for better performance
client.databases.schema.createIndex('posts', ['author_id']);
client.databases.schema.createIndex('posts', ['slug'], { "unique": true });
client.databases.schema.createIndex('posts', ['status', 'published_at']);
client.databases.schema.createIndex('comments', ['post_id']);
client.databases.schema.createIndex('comments', ['user_id']);
import { DatabaseType } from '@ductape/sdk';
// Create database
await ductape.databases.create({
["name"] = "BlogDB",
["tag"] = "blog-db",
type: DatabaseType.POSTGRESQL,
envs: [
{
["slug"] = "prd",
connection_url: Environment.GetEnvironmentVariable("DATABASE_URL")!
}
]
});
// Connect to database
await ductape.Database.Connect(new Dictionary<string, object?>
{
["database"] = "blog-db"
});
// Create users table
await ductape.databases.schema.create('users', {
name: { ["type"] = "String", ["required"] = true },
email: { ["type"] = "String", ["unique"] = true, ["required"] = true },
password: { ["type"] = "String", ["required"] = true },
bio: { ["type"] = "String" },
avatar_url: { ["type"] = "String" },
role: { ["type"] = "String", ["default"] = "user" }, // user, editor, admin
created_at: { ["type"] = "Date", ["default"] = "now" },
updated_at: { ["type"] = "Date", ["default"] = "now" }
});
// Create posts table
await ductape.databases.schema.create('posts', {
title: { ["type"] = "String", ["required"] = true },
slug: { ["type"] = "String", ["unique"] = true, ["required"] = true },
content: { ["type"] = "String", ["required"] = true },
excerpt: { ["type"] = "String" },
featured_image: { ["type"] = "String" },
author_id: { ["type"] = "String", ["required"] = true },
status: { ["type"] = "String", ["default"] = "draft" }, // draft, published, archived
tags: { ["type"] = "Array" },
view_count: { ["type"] = "Number", ["default"] = 0 },
published_at: { ["type"] = "Date" },
created_at: { ["type"] = "Date", ["default"] = "now" },
updated_at: { ["type"] = "Date", ["default"] = "now" }
});
// Create comments table
await ductape.databases.schema.create('comments', {
post_id: { ["type"] = "String", ["required"] = true },
user_id: { ["type"] = "String", ["required"] = true },
content: { ["type"] = "String", ["required"] = true },
parent_id: { ["type"] = "String" }, // For nested replies
created_at: { ["type"] = "Date", ["default"] = "now" },
updated_at: { ["type"] = "Date", ["default"] = "now" }
});
// Create indexes for better performance
await ductape.databases.schema.createIndex('posts', ['author_id']);
await ductape.databases.schema.createIndex('posts', ['slug'], { ["unique"] = true });
await ductape.databases.schema.createIndex('posts', ['status', 'published_at']);
await ductape.databases.schema.createIndex('comments', ['post_id']);
await ductape.databases.schema.createIndex('comments', ['user_id']);
Step 3: Implement User Authentication
- TypeScript
- Java
- Go
- .NET
import bcrypt from 'bcrypt';
// Register new user
export async function registerUser(data: {
name: string;
email: string;
password: string;
}) {
// Hash password
const hashedPassword = await bcrypt.hash(data.password, 10);
// Create user
const result = await ductape.databases.insert({
table: 'users',
data: {
name: data.name,
email: data.email,
password: hashedPassword,
role: 'user'
}
});
const user = result.rows[0];
// Create session
const session = await ductape.sessions.create({
userId: user.id,
data: {
name: user.name,
email: user.email,
role: user.role
},
expiresIn: '7d'
});
return { user, session };
}
// Login user
export async function loginUser(email: string, password: string) {
// Find user
const result = await ductape.databases.query({
table: 'users',
where: { email },
limit: 1
});
if (result.count === 0) {
throw new Error('Invalid credentials');
}
const user = result.rows[0];
// Verify password
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
throw new Error('Invalid credentials');
}
// Create session
const session = await ductape.sessions.create({
userId: user.id,
data: {
name: user.name,
email: user.email,
role: user.role
},
expiresIn: '7d'
});
return { user, session };
}
// Verify session middleware
export async function verifySession(sessionToken: string) {
try {
const session = await ductape.sessions.verify(sessionToken);
return session;
} catch (error) {
throw new Error('Invalid or expired session');
}
}
import bcrypt from 'bcrypt';
// Register new user
export async function registerUser(data: Map.of(
name: string;
email: string;
password: string;
)) Map.of(
// Hash password
Map<String, Object> hashedPassword = bcrypt.hash(data.password, 10);
// Create user
Map<String, Object> result = ductape.databases().insert(Map<String, Object>.of(
"table", "users",
data: Map.of(
name: data.name,
email: data.email,
password: hashedPassword,
"role", "user"
)
));
Map<String, Object> user = result.rows[0];
// Create session
Map<String, Object> session = ductape.sessions.create(Map.of(
userId: user.id,
data: Map.of(
name: user.name,
email: user.email,
role: user.role
),
"expiresIn", "7d"
));
return Map.of( user, session );
)
// Login user
export async function loginUser(email: string, password: string) Map.of(
// Find user
Map<String, Object> result = ductape.databases.query(Map.of(
"table", "users",
where: Map.of( email ),
"limit", 1
));
if (result.count === 0) Map.of(
throw new Error('Invalid credentials');
)
Map<String, Object> user = result.rows[0];
// Verify password
Map<String, Object> isValid = bcrypt.compare(password, user.password);
if (!isValid) Map.of(
throw new Error('Invalid credentials');
)
// Create session
Map<String, Object> session = ductape.sessions.create(Map.of(
userId: user.id,
data: Map.of(
name: user.name,
email: user.email,
role: user.role
),
"expiresIn", "7d"
));
return Map.of( user, session );
)
// Verify session middleware
export async function verifySession(sessionToken: string) Map.of(
try Map.of(
Map<String, Object> session = ductape.sessions.verify(sessionToken);
return session;
) catch (error) Map.of(
throw new Error('Invalid or expired session');
)
)
import "context"
import bcrypt from 'bcrypt';
// Register new user
export async function registerUser(data: {
name: string;
email: string;
password: string;
}) {
// Hash password
hashedPassword := bcrypt.hash(data.password, 10);
// Create user
result := client.Databases.Insert(ctx, map[string]any{
"table": "users",
data: {
name: data.name,
email: data.email,
password: hashedPassword,
"role": "user"
}
});
user := result.rows[0];
// Create session
session := client.sessions.create({
userId: user.id,
data: {
name: user.name,
email: user.email,
role: user.role
},
"expiresIn": "7d"
});
return { user, session };
}
// Login user
export async function loginUser(email: string, password: string) {
// Find user
result := client.databases.query({
"table": "users",
where: { email },
"limit": 1
});
if (result.count === 0) {
throw new Error('Invalid credentials');
}
user := result.rows[0];
// Verify password
isValid := bcrypt.compare(password, user.password);
if (!isValid) {
throw new Error('Invalid credentials');
}
// Create session
session := client.sessions.create({
userId: user.id,
data: {
name: user.name,
email: user.email,
role: user.role
},
"expiresIn": "7d"
});
return { user, session };
}
// Verify session middleware
export async function verifySession(sessionToken: string) {
try {
session := client.sessions.verify(sessionToken);
return session;
} catch (error) {
throw new Error('Invalid or expired session');
}
}
import bcrypt from 'bcrypt';
// Register new user
export async function registerUser(data: {
name: string;
email: string;
password: string;
}) {
// Hash password
var hashedPassword = await bcrypt.hash(data.password, 10);
// Create user
var result = await ductape.Database.Insert(new Dictionary<string, object?>
{
["table"] = "users",
data: {
name: data.name,
email: data.email,
password: hashedPassword,
["role"] = "user"
}
});
var user = result.rows[0];
// Create session
var session = await ductape.sessions.create({
userId: user.id,
data: {
name: user.name,
email: user.email,
role: user.role
},
["expiresIn"] = "7d"
});
return { user, session };
}
// Login user
export async function loginUser(email: string, password: string) {
// Find user
var result = await ductape.databases.query({
["table"] = "users",
where: { email },
["limit"] = 1
});
if (result.count === 0) {
throw new Error('Invalid credentials');
}
var user = result.rows[0];
// Verify password
var isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
throw new Error('Invalid credentials');
}
// Create session
var session = await ductape.sessions.create({
userId: user.id,
data: {
name: user.name,
email: user.email,
role: user.role
},
["expiresIn"] = "7d"
});
return { user, session };
}
// Verify session middleware
export async function verifySession(sessionToken: string) {
try {
var session = await ductape.sessions.verify(sessionToken);
return session;
} catch (error) {
throw new Error('Invalid or expired session');
}
}
Step 4: Create Blog Post Endpoints
- TypeScript
- Java
- Go
- .NET
// Create a new post
export async function createPost(
userId: string,
data: {
title: string;
content: string;
excerpt?: string;
tags?: string[];
featured_image?: string;
}
) {
// Generate slug from title
const slug = data.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
// Insert post
const result = await ductape.databases.insert({
table: 'posts',
data: {
title: data.title,
slug,
content: data.content,
excerpt: data.excerpt || data.content.substring(0, 200),
featured_image: data.featured_image,
author_id: userId,
tags: data.tags || [],
status: 'draft'
}
});
return result.rows[0];
}
// Publish a post
export async function publishPost(postId: string, userId: string) {
// Verify ownership
const post = await ductape.databases.query({
table: 'posts',
where: { id: postId, author_id: userId },
limit: 1
});
if (post.count === 0) {
throw new Error('Post not found or access denied');
}
// Update status
await ductape.databases.update({
table: 'posts',
where: { id: postId },
data: {
status: 'published',
published_at: new Date()
}
});
return { success: true };
}
// Get published posts with pagination
export async function getPosts(page: number = 1, limit: number = 10) {
const offset = (page - 1) * limit;
const result = await ductape.databases.query({
table: 'posts',
where: { status: 'published' },
orderBy: [{ column: 'published_at', order: 'desc' }],
limit,
offset
});
return {
posts: result.rows,
total: result.count,
page,
pages: Math.ceil(result.count / limit)
};
}
// Get single post by slug
export async function getPostBySlug(slug: string) {
const result = await ductape.databases.query({
table: 'posts',
where: { slug, status: 'published' },
limit: 1
});
if (result.count === 0) {
throw new Error('Post not found');
}
const post = result.rows[0];
// Increment view count
await ductape.databases.update({
table: 'posts',
where: { id: post.id },
data: {
view_count: post.view_count + 1
}
});
// Get author info
const authorResult = await ductape.databases.query({
table: 'users',
where: { id: post.author_id },
limit: 1
});
return {
...post,
author: authorResult.rows[0]
};
}
// Update post
export async function updatePost(
postId: string,
userId: string,
data: Partial<{
title: string;
content: string;
excerpt: string;
tags: string[];
featured_image: string;
}>
) {
// Verify ownership
const post = await ductape.databases.query({
table: 'posts',
where: { id: postId, author_id: userId },
limit: 1
});
if (post.count === 0) {
throw new Error('Post not found or access denied');
}
await ductape.databases.update({
table: 'posts',
where: { id: postId },
data: {
...data,
updated_at: new Date()
}
});
return { success: true };
}
// Delete post
export async function deletePost(postId: string, userId: string) {
// Verify ownership or admin role
const post = await ductape.databases.query({
table: 'posts',
where: { id: postId, author_id: userId },
limit: 1
});
if (post.count === 0) {
throw new Error('Post not found or access denied');
}
// Delete associated comments first
await ductape.databases.delete({
table: 'comments',
where: { post_id: postId }
});
// Delete post
await ductape.databases.delete({
table: 'posts',
where: { id: postId }
});
return { success: true };
}
// Create a new post
export async function createPost(
userId: string,
data: Map.of(
title: string;
content: string;
excerpt?: string;
tags?: string[];
featured_image?: string;
)
) Map.of(
// Generate slug from title
Map<String, Object> slug = data.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
// Insert post
Map<String, Object> result = ductape.databases().insert(Map<String, Object>.of(
"table", "posts",
data: Map.of(
title: data.title,
slug,
content: data.content,
excerpt: data.excerpt || data.content.substring(0, 200),
featured_image: data.featured_image,
author_id: userId,
tags: data.tags || [],
"status", "draft"
)
));
return result.rows[0];
)
// Publish a post
export async function publishPost(postId: string, userId: string) Map.of(
// Verify ownership
Map<String, Object> post = ductape.databases.query(Map.of(
"table", "posts",
where: Map.of( id: postId, author_id: userId ),
"limit", 1
));
if (post.count === 0) Map.of(
throw new Error('Post not found or access denied');
)
// Update status
ductape.databases.update(Map.of(
"table", "posts",
where: Map.of( id: postId ),
data: Map.of(
"status", "published",
published_at: Instant.now()
)
));
return Map.of( "success", true );
)
// Get published posts with pagination
export async function getPosts(page: number = 1, limit: number = 10) Map.of(
Map<String, Object> offset = (page - 1) * limit;
Map<String, Object> result = ductape.databases.query(Map.of(
"table", "posts",
where: Map.of( "status", "published" ),
orderBy: [Map.of( "column", "published_at", "order", "desc" )],
limit,
offset
));
return Map.of(
posts: result.rows,
total: result.count,
page,
pages: Math.ceil(result.count / limit)
);
)
// Get single post by slug
export async function getPostBySlug(slug: string) Map.of(
Map<String, Object> result = ductape.databases.query(Map.of(
"table", "posts",
where: Map.of( slug, "status", "published" ),
"limit", 1
));
if (result.count === 0) Map.of(
throw new Error('Post not found');
)
Map<String, Object> post = result.rows[0];
// Increment view count
ductape.databases.update(Map.of(
"table", "posts",
where: Map.of( id: post.id ),
data: Map.of(
view_count: post.view_count + 1
)
));
// Get author info
Map<String, Object> authorResult = ductape.databases.query(Map.of(
"table", "users",
where: Map.of( id: post.author_id ),
"limit", 1
));
return Map.of(
...post,
author: authorResult.rows[0]
);
)
// Update post
export async function updatePost(
postId: string,
userId: string,
data: Partial<Map.of(
title: string;
content: string;
excerpt: string;
tags: string[];
featured_image: string;
)>
) Map.of(
// Verify ownership
Map<String, Object> post = ductape.databases.query(Map.of(
"table", "posts",
where: Map.of( id: postId, author_id: userId ),
"limit", 1
));
if (post.count === 0) Map.of(
throw new Error('Post not found or access denied');
)
ductape.databases.update(Map.of(
"table", "posts",
where: Map.of( id: postId ),
data: Map.of(
...data,
updated_at: Instant.now()
)
));
return Map.of( "success", true );
)
// Delete post
export async function deletePost(postId: string, userId: string) Map.of(
// Verify ownership or admin role
Map<String, Object> post = ductape.databases.query(Map.of(
"table", "posts",
where: Map.of( id: postId, author_id: userId ),
"limit", 1
));
if (post.count === 0) Map.of(
throw new Error('Post not found or access denied');
)
// Delete associated comments first
ductape.databases.delete(Map.of(
"table", "comments",
where: Map.of( post_id: postId )
));
// Delete post
ductape.databases.delete(Map.of(
"table", "posts",
where: Map.of( id: postId )
));
return Map.of( "success", true );
)
import "context"
// Create a new post
export async function createPost(
userId: string,
data: {
title: string;
content: string;
excerpt?: string;
tags?: string[];
featured_image?: string;
}
) {
// Generate slug from title
slug := data.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
// Insert post
result := client.Databases.Insert(ctx, map[string]any{
"table": "posts",
data: {
title: data.title,
slug,
content: data.content,
excerpt: data.excerpt || data.content.substring(0, 200),
featured_image: data.featured_image,
author_id: userId,
tags: data.tags || [],
"status": "draft"
}
});
return result.rows[0];
}
// Publish a post
export async function publishPost(postId: string, userId: string) {
// Verify ownership
post := client.databases.query({
"table": "posts",
where: { id: postId, author_id: userId },
"limit": 1
});
if (post.count === 0) {
throw new Error('Post not found or access denied');
}
// Update status
client.databases.update({
"table": "posts",
where: { id: postId },
data: {
"status": "published",
published_at: new Date()
}
});
return { "success": true };
}
// Get published posts with pagination
export async function getPosts(page: number = 1, limit: number = 10) {
offset := (page - 1) * limit;
result := client.databases.query({
"table": "posts",
where: { "status": "published" },
orderBy: [{ "column": "published_at", "order": "desc" }],
limit,
offset
});
return {
posts: result.rows,
total: result.count,
page,
pages: Math.ceil(result.count / limit)
};
}
// Get single post by slug
export async function getPostBySlug(slug: string) {
result := client.databases.query({
"table": "posts",
where: { slug, "status": "published" },
"limit": 1
});
if (result.count === 0) {
throw new Error('Post not found');
}
post := result.rows[0];
// Increment view count
client.databases.update({
"table": "posts",
where: { id: post.id },
data: {
view_count: post.view_count + 1
}
});
// Get author info
authorResult := client.databases.query({
"table": "users",
where: { id: post.author_id },
"limit": 1
});
return {
...post,
author: authorResult.rows[0]
};
}
// Update post
export async function updatePost(
postId: string,
userId: string,
data: Partial<{
title: string;
content: string;
excerpt: string;
tags: string[];
featured_image: string;
}>
) {
// Verify ownership
post := client.databases.query({
"table": "posts",
where: { id: postId, author_id: userId },
"limit": 1
});
if (post.count === 0) {
throw new Error('Post not found or access denied');
}
client.databases.update({
"table": "posts",
where: { id: postId },
data: {
...data,
updated_at: new Date()
}
});
return { "success": true };
}
// Delete post
export async function deletePost(postId: string, userId: string) {
// Verify ownership or admin role
post := client.databases.query({
"table": "posts",
where: { id: postId, author_id: userId },
"limit": 1
});
if (post.count === 0) {
throw new Error('Post not found or access denied');
}
// Delete associated comments first
client.databases.delete({
"table": "comments",
where: { post_id: postId }
});
// Delete post
client.databases.delete({
"table": "posts",
where: { id: postId }
});
return { "success": true };
}
// Create a new post
export async function createPost(
userId: string,
data: {
title: string;
content: string;
excerpt?: string;
tags?: string[];
featured_image?: string;
}
) {
// Generate slug from title
var slug = data.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
// Insert post
var result = await ductape.Database.Insert(new Dictionary<string, object?>
{
["table"] = "posts",
data: {
title: data.title,
slug,
content: data.content,
excerpt: data.excerpt || data.content.substring(0, 200),
featured_image: data.featured_image,
author_id: userId,
tags: data.tags || [],
["status"] = "draft"
}
});
return result.rows[0];
}
// Publish a post
export async function publishPost(postId: string, userId: string) {
// Verify ownership
var post = await ductape.databases.query({
["table"] = "posts",
where: { id: postId, author_id: userId },
["limit"] = 1
});
if (post.count === 0) {
throw new Error('Post not found or access denied');
}
// Update status
await ductape.databases.update({
["table"] = "posts",
where: { id: postId },
data: {
["status"] = "published",
published_at: DateTime.UtcNow
}
});
return { ["success"] = true };
}
// Get published posts with pagination
export async function getPosts(page: number = 1, limit: number = 10) {
var offset = (page - 1) * limit;
var result = await ductape.databases.query({
["table"] = "posts",
where: { ["status"] = "published" },
orderBy: [{ ["column"] = "published_at", ["order"] = "desc" }],
limit,
offset
});
return {
posts: result.rows,
total: result.count,
page,
pages: Math.ceil(result.count / limit)
};
}
// Get single post by slug
export async function getPostBySlug(slug: string) {
var result = await ductape.databases.query({
["table"] = "posts",
where: { slug, ["status"] = "published" },
["limit"] = 1
});
if (result.count === 0) {
throw new Error('Post not found');
}
var post = result.rows[0];
// Increment view count
await ductape.databases.update({
["table"] = "posts",
where: { id: post.id },
data: {
view_count: post.view_count + 1
}
});
// Get author info
var authorResult = await ductape.databases.query({
["table"] = "users",
where: { id: post.author_id },
["limit"] = 1
});
return {
...post,
author: authorResult.rows[0]
};
}
// Update post
export async function updatePost(
postId: string,
userId: string,
data: Partial<{
title: string;
content: string;
excerpt: string;
tags: string[];
featured_image: string;
}>
) {
// Verify ownership
var post = await ductape.databases.query({
["table"] = "posts",
where: { id: postId, author_id: userId },
["limit"] = 1
});
if (post.count === 0) {
throw new Error('Post not found or access denied');
}
await ductape.databases.update({
["table"] = "posts",
where: { id: postId },
data: {
...data,
updated_at: DateTime.UtcNow
}
});
return { ["success"] = true };
}
// Delete post
export async function deletePost(postId: string, userId: string) {
// Verify ownership or admin role
var post = await ductape.databases.query({
["table"] = "posts",
where: { id: postId, author_id: userId },
["limit"] = 1
});
if (post.count === 0) {
throw new Error('Post not found or access denied');
}
// Delete associated comments first
await ductape.databases.delete({
["table"] = "comments",
where: { post_id: postId }
});
// Delete post
await ductape.databases.delete({
["table"] = "posts",
where: { id: postId }
});
return { ["success"] = true };
}
Step 5: Implement Comment System
- TypeScript
- Java
- Go
- .NET
// Add comment to post
export async function addComment(
userId: string,
postId: string,
content: string,
parentId?: string
) {
// Verify post exists
const post = await ductape.databases.query({
table: 'posts',
where: { id: postId, status: 'published' },
limit: 1
});
if (post.count === 0) {
throw new Error('Post not found');
}
const result = await ductape.databases.insert({
table: 'comments',
data: {
post_id: postId,
user_id: userId,
content,
parent_id: parentId
}
});
return result.rows[0];
}
// Get comments for post
export async function getComments(postId: string) {
const result = await ductape.databases.query({
table: 'comments',
where: { post_id: postId },
orderBy: [{ column: 'created_at', order: 'asc' }]
});
// Get user info for each comment
const userIds = [...new Set(result.rows.map(c => c.user_id))];
const users = await ductape.databases.query({
table: 'users',
where: { id: { $in: userIds } }
});
const userMap = new Map(users.rows.map(u => [u.id, u]));
// Build nested comment structure
const comments = result.rows.map(comment => ({
...comment,
user: userMap.get(comment.user_id)
}));
// Organize into threads
const commentMap = new Map(comments.map(c => [c.id, { ...c, replies: [] }]));
const rootComments: any[] = [];
comments.forEach(comment => {
if (comment.parent_id) {
const parent = commentMap.get(comment.parent_id);
if (parent) {
parent.replies.push(commentMap.get(comment.id));
}
} else {
rootComments.push(commentMap.get(comment.id));
}
});
return rootComments;
}
// Add comment to post
export async function addComment(
userId: string,
postId: string,
content: string,
parentId?: string
) Map.of(
// Verify post exists
Map<String, Object> post = ductape.databases.query(Map.of(
"table", "posts",
where: Map.of( id: postId, "status", "published" ),
"limit", 1
));
if (post.count === 0) Map.of(
throw new Error('Post not found');
)
Map<String, Object> result = ductape.databases().insert(Map<String, Object>.of(
"table", "comments",
data: Map.of(
post_id: postId,
user_id: userId,
content,
parent_id: parentId
)
));
return result.rows[0];
)
// Get comments for post
export async function getComments(postId: string) Map.of(
Map<String, Object> result = ductape.databases.query(Map.of(
"table", "comments",
where: Map.of( post_id: postId ),
orderBy: [Map.of( "column", "created_at", "order", "asc" )]
));
// Get user info for each comment
Map<String, Object> userIds = [...new Set(result.rows.map(c => c.user_id))];
Map<String, Object> users = ductape.databases.query(Map.of(
"table", "users",
where: Map.of( id: Map.of( $in: userIds ) )
));
Map<String, Object> userMap = new Map(users.rows.map(u => [u.id, u]));
// Build nested comment structure
Map<String, Object> comments = result.rows.map(comment => (Map.of(
...comment,
user: userMap.get(comment.user_id)
)));
// Organize into threads
Map<String, Object> commentMap = new Map(comments.map(c => [c.id, Map.of( ...c, replies: [] )]));
Map<String, Object> rootComments: any[] = [];
comments.forEach(comment => Map.of(
if (comment.parent_id) Map.of(
Map<String, Object> parent = commentMap.get(comment.parent_id);
if (parent) Map.of(
parent.replies.push(commentMap.get(comment.id));
)
) else Map.of(
rootComments.push(commentMap.get(comment.id));
)
));
return rootComments;
)
import "context"
// Add comment to post
export async function addComment(
userId: string,
postId: string,
content: string,
parentId?: string
) {
// Verify post exists
post := client.databases.query({
"table": "posts",
where: { id: postId, "status": "published" },
"limit": 1
});
if (post.count === 0) {
throw new Error('Post not found');
}
result := client.Databases.Insert(ctx, map[string]any{
"table": "comments",
data: {
post_id: postId,
user_id: userId,
content,
parent_id: parentId
}
});
return result.rows[0];
}
// Get comments for post
export async function getComments(postId: string) {
result := client.databases.query({
"table": "comments",
where: { post_id: postId },
orderBy: [{ "column": "created_at", "order": "asc" }]
});
// Get user info for each comment
userIds := [...new Set(result.rows.map(c => c.user_id))];
users := client.databases.query({
"table": "users",
where: { id: { $in: userIds } }
});
userMap := new Map(users.rows.map(u => [u.id, u]));
// Build nested comment structure
comments := result.rows.map(comment => ({
...comment,
user: userMap.get(comment.user_id)
}));
// Organize into threads
commentMap := new Map(comments.map(c => [c.id, { ...c, replies: [] }]));
const rootComments: any[] = [];
comments.forEach(comment => {
if (comment.parent_id) {
parent := commentMap.get(comment.parent_id);
if (parent) {
parent.replies.push(commentMap.get(comment.id));
}
} else {
rootComments.push(commentMap.get(comment.id));
}
});
return rootComments;
}
// Add comment to post
export async function addComment(
userId: string,
postId: string,
content: string,
parentId?: string
) {
// Verify post exists
var post = await ductape.databases.query({
["table"] = "posts",
where: { id: postId, ["status"] = "published" },
["limit"] = 1
});
if (post.count === 0) {
throw new Error('Post not found');
}
var result = await ductape.Database.Insert(new Dictionary<string, object?>
{
["table"] = "comments",
data: {
post_id: postId,
user_id: userId,
content,
parent_id: parentId
}
});
return result.rows[0];
}
// Get comments for post
export async function getComments(postId: string) {
var result = await ductape.databases.query({
["table"] = "comments",
where: { post_id: postId },
orderBy: [{ ["column"] = "created_at", ["order"] = "asc" }]
});
// Get user info for each comment
var userIds = [...new Set(result.rows.map(c => c.user_id))];
var users = await ductape.databases.query({
["table"] = "users",
where: { id: { $in: userIds } }
});
var userMap = new Map(users.rows.map(u => [u.id, u]));
// Build nested comment structure
var comments = result.rows.map(comment => ({
...comment,
user: userMap.get(comment.user_id)
}));
// Organize into threads
var commentMap = new Map(comments.map(c => [c.id, { ...c, replies: [] }]));
var rootComments: any[] = [];
comments.forEach(comment => {
if (comment.parent_id) {
var parent = commentMap.get(comment.parent_id);
if (parent) {
parent.replies.push(commentMap.get(comment.id));
}
} else {
rootComments.push(commentMap.get(comment.id));
}
});
return rootComments;
}
Step 6: File Upload for Images
- TypeScript
- Java
- Go
- .NET
// Upload featured image
export async function uploadFeaturedImage(file: File) {
const result = await ductape.storage.upload({
file,
path: 'blog/featured-images',
metadata: {
type: 'featured-image'
}
});
return result.url;
}
// Delete image
export async function deleteImage(fileId: string) {
await ductape.storage.delete({ fileId });
return { success: true };
}
// Upload featured image
export async function uploadFeaturedImage(file: File) Map.of(
Map<String, Object> result = ductape.storage.upload(Map.of(
file,
"path", "blog/featured-images",
metadata: Map.of(
"type", "featured-image"
)
));
return result.url;
)
// Delete image
export async function deleteImage(fileId: string) Map.of(
ductape.storage.delete(Map.of( fileId ));
return Map.of( "success", true );
)
// Upload featured image
export async function uploadFeaturedImage(file: File) {
result := client.storage.upload({
file,
"path": "blog/featured-images",
metadata: {
"type": "featured-image"
}
});
return result.url;
}
// Delete image
export async function deleteImage(fileId: string) {
client.storage.delete({ fileId });
return { "success": true };
}
// Upload featured image
export async function uploadFeaturedImage(file: File) {
var result = await ductape.storage.upload({
file,
["path"] = "blog/featured-images",
metadata: {
["type"] = "featured-image"
}
});
return result.url;
}
// Delete image
export async function deleteImage(fileId: string) {
await ductape.storage.delete({ fileId });
return { ["success"] = true };
}
Step 7: Search and Filtering
- TypeScript
- Java
- Go
- .NET
// Search posts
export async function searchPosts(query: string, tags?: string[]) {
const where: any = {
status: 'published',
$or: [
{ title: { $like: `%${query}%` } },
{ content: { $like: `%${query}%` } }
]
};
if (tags && tags.length > 0) {
where.tags = { $containsAny: tags };
}
const result = await ductape.databases.query({
table: 'posts',
where,
orderBy: [{ column: 'published_at', order: 'desc' }],
limit: 20
});
return result.rows;
}
// Get posts by tag
export async function getPostsByTag(tag: string) {
const result = await ductape.databases.query({
table: 'posts',
where: {
status: 'published',
tags: { $contains: tag }
},
orderBy: [{ column: 'published_at', order: 'desc' }]
});
return result.rows;
}
// Search posts
export async function searchPosts(query: string, tags?: string[]) Map.of(
Map<String, Object> where: any = Map.of(
"status", "published",
$or: [
Map.of( title: Map.of( $like: `%$Map.of(query)%` ) ),
Map.of( content: Map.of( $like: `%$Map.of(query)%` ) )
]
);
if (tags && tags.length > 0) Map.of(
where.tags = Map.of( $containsAny: tags );
)
Map<String, Object> result = ductape.databases.query(Map.of(
"table", "posts",
where,
orderBy: [Map.of( "column", "published_at", "order", "desc" )],
"limit", 20
));
return result.rows;
)
// Get posts by tag
export async function getPostsByTag(tag: string) Map.of(
Map<String, Object> result = ductape.databases.query(Map.of(
"table", "posts",
where: Map.of(
"status", "published",
tags: Map.of( $contains: tag )
),
orderBy: [Map.of( "column", "published_at", "order", "desc" )]
));
return result.rows;
)
// Search posts
export async function searchPosts(query: string, tags?: string[]) {
const where: any = {
"status": "published",
$or: [
{ title: { $like: `%${query}%` } },
{ content: { $like: `%${query}%` } }
]
};
if (tags && tags.length > 0) {
where.tags = { $containsAny: tags };
}
result := client.databases.query({
"table": "posts",
where,
orderBy: [{ "column": "published_at", "order": "desc" }],
"limit": 20
});
return result.rows;
}
// Get posts by tag
export async function getPostsByTag(tag: string) {
result := client.databases.query({
"table": "posts",
where: {
"status": "published",
tags: { $contains: tag }
},
orderBy: [{ "column": "published_at", "order": "desc" }]
});
return result.rows;
}
// Search posts
export async function searchPosts(query: string, tags?: string[]) {
var where: any = {
["status"] = "published",
$or: [
{ title: { $like: `%${query}%` } },
{ content: { $like: `%${query}%` } }
]
};
if (tags && tags.length > 0) {
where.tags = { $containsAny: tags };
}
var result = await ductape.databases.query({
["table"] = "posts",
where,
orderBy: [{ ["column"] = "published_at", ["order"] = "desc" }],
["limit"] = 20
});
return result.rows;
}
// Get posts by tag
export async function getPostsByTag(tag: string) {
var result = await ductape.databases.query({
["table"] = "posts",
where: {
["status"] = "published",
tags: { $contains: tag }
},
orderBy: [{ ["column"] = "published_at", ["order"] = "desc" }]
});
return result.rows;
}
Step 8: Notifications
- TypeScript
- Java
- Go
- .NET
// Notify when new comment
export async function notifyNewComment(
postId: string,
commentUserId: string
) {
// Get post and author
const post = await ductape.databases.query({
table: 'posts',
where: { id: postId },
limit: 1
});
const author = await ductape.databases.query({
table: 'users',
where: { id: post.rows[0].author_id },
limit: 1
});
// Don't notify if author is commenting on own post
if (author.rows[0].id === commentUserId) {
return;
}
await ductape.notifications.send({
channel: 'email',
to: author.rows[0].email,
template: 'new-comment',
data: {
postTitle: post.rows[0].title,
postUrl: `https://myblog.com/posts/${post.rows[0].slug}`
}
});
}
// Notify when new comment
export async function notifyNewComment(
postId: string,
commentUserId: string
) Map.of(
// Get post and author
Map<String, Object> post = ductape.databases.query(Map.of(
"table", "posts",
where: Map.of( id: postId ),
"limit", 1
));
Map<String, Object> author = ductape.databases.query(Map.of(
"table", "users",
where: Map.of( id: post.rows[0].author_id ),
"limit", 1
));
// Don't notify if author is commenting on own post
if (author.rows[0].id === commentUserId) Map.of(
return;
)
ductape.notifications().send(Map<String, Object>.of(
"channel", "email",
to: author.rows[0].email,
"template", "new-comment",
data: Map.of(
postTitle: post.rows[0].title,
postUrl: `https://myblog.com/posts/$Map.of(post.rows[0].slug)`
)
));
)
import "context"
// Notify when new comment
export async function notifyNewComment(
postId: string,
commentUserId: string
) {
// Get post and author
post := client.databases.query({
"table": "posts",
where: { id: postId },
"limit": 1
});
author := client.databases.query({
"table": "users",
where: { id: post.rows[0].author_id },
"limit": 1
});
// Don't notify if author is commenting on own post
if (author.rows[0].id === commentUserId) {
return;
}
client.Notifications.Send(ctx, map[string]any{
"channel": "email",
to: author.rows[0].email,
"template": "new-comment",
data: {
postTitle: post.rows[0].title,
postUrl: `https://myblog.com/posts/${post.rows[0].slug}`
}
});
}
// Notify when new comment
export async function notifyNewComment(
postId: string,
commentUserId: string
) {
// Get post and author
var post = await ductape.databases.query({
["table"] = "posts",
where: { id: postId },
["limit"] = 1
});
var author = await ductape.databases.query({
["table"] = "users",
where: { id: post.rows[0].author_id },
["limit"] = 1
});
// Don't notify if author is commenting on own post
if (author.rows[0].id === commentUserId) {
return;
}
await await ductape.Notifications.SendAsync(new Dictionary<string, object?>
{
["channel"] = "email",
to: author.rows[0].email,
["template"] = "new-comment",
data: {
postTitle: post.rows[0].title,
postUrl: `https://myblog.com/posts/${post.rows[0].slug}`
}
});
}
Complete Express.js Integration
import express from 'express';
const app = express();
app.use(express.json());
// Middleware to verify session
const authenticate = async (req: any, res: any, next: any) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
const session = await verifySession(token);
req.user = session.data;
req.userId = session.userId;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid session' });
}
};
// Auth routes
app.post('/auth/register', async (req, res) => {
try {
const result = await registerUser(req.body);
res.json(result);
} catch (error: any) {
res.status(400).json({ error: error.message });
}
});
app.post('/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
const result = await loginUser(email, password);
res.json(result);
} catch (error: any) {
res.status(401).json({ error: error.message });
}
});
// Post routes
app.get('/posts', async (req, res) => {
const page = parseInt(req.query.page as string) || 1;
const result = await getPosts(page);
res.json(result);
});
app.get('/posts/:slug', async (req, res) => {
try {
const post = await getPostBySlug(req.params.slug);
res.json(post);
} catch (error: any) {
res.status(404).json({ error: error.message });
}
});
app.post('/posts', authenticate, async (req, res) => {
try {
const post = await createPost(req.userId, req.body);
res.json(post);
} catch (error: any) {
res.status(400).json({ error: error.message });
}
});
app.put('/posts/:id', authenticate, async (req, res) => {
try {
await updatePost(req.params.id, req.userId, req.body);
res.json({ success: true });
} catch (error: any) {
res.status(400).json({ error: error.message });
}
});
app.post('/posts/:id/publish', authenticate, async (req, res) => {
try {
await publishPost(req.params.id, req.userId);
res.json({ success: true });
} catch (error: any) {
res.status(400).json({ error: error.message });
}
});
// Comment routes
app.get('/posts/:id/comments', async (req, res) => {
const comments = await getComments(req.params.id);
res.json(comments);
});
app.post('/posts/:id/comments', authenticate, async (req, res) => {
try {
const comment = await addComment(
req.userId,
req.params.id,
req.body.content,
req.body.parent_id
);
// Notify post author
await notifyNewComment(req.params.id, req.userId);
res.json(comment);
} catch (error: any) {
res.status(400).json({ error: error.message });
}
});
app.listen(3000, () => {
console.log('Blog API running on port 3000');
});
Next Steps
- Add rate limiting for API endpoints
- Implement caching for frequently accessed posts
- Add full-text search with PostgreSQL
- Implement draft autosave
- Add post analytics and tracking
- Create RSS feed generation
- Add social media sharing