Skip to content

30-Minute Tutorial

Build a complete Task Management API with Flowfull in just 30 minutes! This tutorial will guide you through creating a production-ready backend with authentication, database, and caching.

What You'll Build

A Task Management API with:

  • ✅ User authentication (via Pubflow)
  • ✅ CRUD operations for tasks
  • ✅ Protected routes
  • ✅ Database integration
  • ✅ Caching for performance
  • ✅ Input validation

Prerequisites

  • ✅ Bun installed (bun.sh)
  • ✅ PostgreSQL running locally
  • ✅ Pubflow account (pubflow.com)
  • ✅ 30 minutes of your time ⏱️

Step 1: Set Up Pubflow (5 minutes)

1.1 Create Flowless Instance

  1. Visit pubflow.com
  2. Sign up or log in
  3. Click "Create Flowless Instance"
  4. Name it: my-tasks-app
  5. Copy your Bridge Secret

Your Flowless URL:

https://my-tasks-app.pubflow.com

1.2 Create Test User

Use the Pubflow dashboard to create a test user:

  • Email: test@example.com
  • Password: Test123!

Step 2: Clone and Configure (5 minutes)

2.1 Clone Template

We'll use flowfull-node, the official Node.js/TypeScript starter kit:

bash
# Clone the official starter kit
git clone https://github.com/pubflow/flowfull-node.git task-api
cd task-api
bun install

TIP

This tutorial uses flowfull-node (Node.js/TypeScript), the official starter kit. Community starter kits for Go, Python, and Rust are coming soon!

2.2 Configure Environment

Create .env:

env
# Flowless Configuration
FLOWLESS_URL=https://my-tasks-app.pubflow.com
BRIDGE_SECRET=your-bridge-secret-here

# Database
DATABASE_TYPE=postgresql
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/tasks_db

# Server
PORT=3000
NODE_ENV=development

# Security
VALIDATION_MODE=STANDARD

# Cache (Optional)
CACHE_ENABLED=true

2.3 Create Database

bash
# Create database
createdb tasks_db

# Run migrations
bun run migrate

Step 3: Create Task Model (5 minutes)

3.1 Create Migration

Create migrations/002_create_tasks.ts:

typescript
import { Kysely } from 'kysely';

export async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable('tasks')
    .addColumn('id', 'varchar(255)', (col) => col.primaryKey())
    .addColumn('user_id', 'varchar(255)', (col) => col.notNull())
    .addColumn('title', 'varchar(255)', (col) => col.notNull())
    .addColumn('description', 'text')
    .addColumn('status', 'varchar(50)', (col) => col.notNull().defaultTo('pending'))
    .addColumn('priority', 'varchar(50)', (col) => col.notNull().defaultTo('medium'))
    .addColumn('due_date', 'timestamp')
    .addColumn('created_at', 'timestamp', (col) => col.notNull())
    .addColumn('updated_at', 'timestamp', (col) => col.notNull())
    .execute();

  // Create index on user_id
  await db.schema
    .createIndex('tasks_user_id_idx')
    .on('tasks')
    .column('user_id')
    .execute();
}

export async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable('tasks').execute();
}

Run migration:

bash
bun run migrate

3.2 Create Task Types

Create src/types/task.ts:

typescript
export interface Task {
  id: string;
  user_id: string;
  title: string;
  description?: string;
  status: 'pending' | 'in_progress' | 'completed';
  priority: 'low' | 'medium' | 'high';
  due_date?: string;
  created_at: string;
  updated_at: string;
}

export interface CreateTaskInput {
  title: string;
  description?: string;
  priority?: 'low' | 'medium' | 'high';
  due_date?: string;
}

export interface UpdateTaskInput {
  title?: string;
  description?: string;
  status?: 'pending' | 'in_progress' | 'completed';
  priority?: 'low' | 'medium' | 'high';
  due_date?: string;
}

Step 4: Create Task Routes (10 minutes)

Create src/routes/tasks.ts:

typescript
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { nanoid } from 'nanoid';
import { requireAuth } from '../lib/auth/middleware';
import { db } from '../lib/database';

const app = new Hono();

// Validation schemas
const createTaskSchema = z.object({
  title: z.string().min(1).max(255),
  description: z.string().optional(),
  priority: z.enum(['low', 'medium', 'high']).optional(),
  due_date: z.string().datetime().optional()
});

const updateTaskSchema = z.object({
  title: z.string().min(1).max(255).optional(),
  description: z.string().optional(),
  status: z.enum(['pending', 'in_progress', 'completed']).optional(),
  priority: z.enum(['low', 'medium', 'high']).optional(),
  due_date: z.string().datetime().optional()
});

// Get all tasks
app.get('/', requireAuth(), async (c) => {
  const userId = c.get('user_id');
  
  const tasks = await db
    .selectFrom('tasks')
    .where('user_id', '=', userId)
    .selectAll()
    .orderBy('created_at', 'desc')
    .execute();
  
  return c.json({ tasks });
});

// Get single task
app.get('/:id', requireAuth(), async (c) => {
  const userId = c.get('user_id');
  const taskId = c.req.param('id');
  
  const task = await db
    .selectFrom('tasks')
    .where('id', '=', taskId)
    .where('user_id', '=', userId)
    .selectAll()
    .executeTakeFirst();
  
  if (!task) {
    return c.json({ error: 'Task not found' }, 404);
  }
  
  return c.json({ task });
});

// Create task
app.post('/', requireAuth(), zValidator('json', createTaskSchema), async (c) => {
  const userId = c.get('user_id');
  const data = c.req.valid('json');
  
  const task = await db
    .insertInto('tasks')
    .values({
      id: nanoid(),
      user_id: userId,
      title: data.title,
      description: data.description,
      priority: data.priority || 'medium',
      status: 'pending',
      due_date: data.due_date,
      created_at: new Date().toISOString(),
      updated_at: new Date().toISOString()
    })
    .returningAll()
    .executeTakeFirst();
  
  return c.json({ task }, 201);
});

// Update task
app.patch('/:id', requireAuth(), zValidator('json', updateTaskSchema), async (c) => {
  const userId = c.get('user_id');
  const taskId = c.req.param('id');
  const data = c.req.valid('json');
  
  const task = await db
    .updateTable('tasks')
    .set({
      ...data,
      updated_at: new Date().toISOString()
    })
    .where('id', '=', taskId)
    .where('user_id', '=', userId)
    .returningAll()
    .executeTakeFirst();
  
  if (!task) {
    return c.json({ error: 'Task not found' }, 404);
  }
  
  return c.json({ task });
});

// Delete task
app.delete('/:id', requireAuth(), async (c) => {
  const userId = c.get('user_id');
  const taskId = c.req.param('id');
  
  const result = await db
    .deleteFrom('tasks')
    .where('id', '=', taskId)
    .where('user_id', '=', userId)
    .executeTakeFirst();
  
  if (result.numDeletedRows === 0n) {
    return c.json({ error: 'Task not found' }, 404);
  }
  
  return c.json({ success: true });
});

export default app;

4.1 Register Routes

Update src/index.ts:

typescript
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import tasksRoutes from './routes/tasks';

const app = new Hono();

app.use('/*', cors());

// Register routes
app.route('/api/tasks', tasksRoutes);

export default app;

Step 5: Test Your API (5 minutes)

5.1 Start Server

bash
bun run dev

Server running at http://localhost:3000

5.2 Login to Get Session

bash
curl -X POST https://my-tasks-app.pubflow.com/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "Test123!"
  }'

Response:

json
{
  "session_id": "abc123...",
  "user": { "id": "user_123", "email": "test@example.com" }
}

5.3 Create a Task

bash
curl -X POST http://localhost:3000/api/tasks \
  -H "Content-Type: application/json" \
  -H "X-Session-Id: abc123..." \
  -d '{
    "title": "Build amazing app",
    "description": "Using Flowfull and Pubflow",
    "priority": "high",
    "due_date": "2024-12-31T23:59:59Z"
  }'

Response:

json
{
  "task": {
    "id": "task_abc123",
    "user_id": "user_123",
    "title": "Build amazing app",
    "description": "Using Flowfull and Pubflow",
    "status": "pending",
    "priority": "high",
    "due_date": "2024-12-31T23:59:59Z",
    "created_at": "2024-01-01T00:00:00Z",
    "updated_at": "2024-01-01T00:00:00Z"
  }
}

5.4 Get All Tasks

bash
curl http://localhost:3000/api/tasks \
  -H "X-Session-Id: abc123..."

5.5 Update Task

bash
curl -X PATCH http://localhost:3000/api/tasks/task_abc123 \
  -H "Content-Type: application/json" \
  -H "X-Session-Id: abc123..." \
  -d '{
    "status": "in_progress"
  }'

5.6 Delete Task

bash
curl -X DELETE http://localhost:3000/api/tasks/task_abc123 \
  -H "X-Session-Id: abc123..."

🎉 Congratulations!

You've built a complete Task Management API in 30 minutes!

What You've Learned

✅ How to connect to Pubflow for authentication ✅ How to create protected routes ✅ How to use Bridge Validation ✅ How to implement CRUD operations ✅ How to validate input with Zod ✅ How to use Kysely ORM

Next Steps

Add More Features

Learn More Concepts

Build Something Bigger

  • Add task categories and tags
  • Implement task sharing between users
  • Add file attachments to tasks
  • Create task templates
  • Build a frontend with React/Next.js

Need Help?

Need Professional Support?

If you need assistance, professional support, or want us to build your backend for you:

  • 🌐 Notside.com - Technology firm specializing in Pubflow implementations
  • 📧 Email: contact@notside.com
  • 💼 Services: Custom backend development, consulting, and enterprise support

Ready to build more?

Explore Guides →

Released under the MIT License.