Back to Todo App

Documentation

API

API Documentation

Complete reference for all API endpoints in the Personal To-Do List app.

🔐 Authentication

All /api/todos/* endpoints require authentication via NextAuth session.

Headers

No special headers required. Authentication is handled via cookies set by NextAuth.

Error Responses

json
{
  "error": "Unauthorized"
}

Status: 401 Unauthorized

📡 API Endpoints

Authentication

#### POST /api/auth/signin

Initiate GitHub OAuth sign-in flow.

Handled by NextAuth - Use the sign-in button on the frontend.

#### POST /api/auth/signout

Sign out the current user.

Handled by NextAuth - Use the sign-out button on the frontend.


Todos

#### GET /api/todos

Get all todos for the authenticated user.

Authentication: Required

Response: 200 OK

json
[
  {
    "id": 1,
    "userId": "github|12345",
    "title": "Buy groceries",
    "comment": "Milk, eggs, bread",
    "order": 0,
    "createdAt": "2025-01-15T10:00:00.000Z",
    "updatedAt": "2025-01-15T10:00:00.000Z"
  },
  {
    "id": 2,
    "userId": "github|12345",
    "title": "Finish project",
    "comment": null,
    "order": 1,
    "createdAt": "2025-01-15T11:00:00.000Z",
    "updatedAt": "2025-01-15T11:00:00.000Z"
  }
]

Error: 401 Unauthorized

json
{
  "error": "Unauthorized"
}

Example:

typescript
const response = await fetch("/api/todos");
const todos = await response.json();


#### POST /api/todos

Create a new todo.

Authentication: Required

Request Body:

json
{
  "title": "Task name",
  "comment": "Optional comment"  // Optional
}

Response: 201 Created

json
{
  "id": 3,
  "userId": "github|12345",
  "title": "Task name",
  "comment": "Optional comment",
  "order": 2,
  "createdAt": "2025-01-15T12:00:00.000Z",
  "updatedAt": "2025-01-15T12:00:00.000Z"
}

Validation Errors: 400 Bad Request

json
{
  "error": "Title is required and must be a string"
}

Example:

typescript
const response = await fetch("/api/todos", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    title: "New task",
    comment: "Optional description",
  }),
});

const newTodo = await response.json();


#### PATCH /api/todos/[id]

Update an existing todo.

Authentication: Required

URL Parameters:

  • id (number) - Todo ID
Request Body (all fields optional):

json
{
  "title": "Updated title",
  "comment": "Updated comment",
  "order": 5
}

Response: 200 OK

json
{
  "id": 1,
  "userId": "github|12345",
  "title": "Updated title",
  "comment": "Updated comment",
  "order": 5,
  "createdAt": "2025-01-15T10:00:00.000Z",
  "updatedAt": "2025-01-15T13:00:00.000Z"
}

Validation Errors: 400 Bad Request

json
{
  "error": "Title must be a non-empty string"
}

Not Found: 404 Not Found

json
{
  "error": "Todo not found"
}

Example:

typescript
const response = await fetch("/api/todos/1", {
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    title: "Updated task name",
  }),
});

const updatedTodo = await response.json();


#### DELETE /api/todos/[id]

Delete a todo.

Authentication: Required

URL Parameters:

  • id (number) - Todo ID
Response: 200 OK

json
{
  "success": true,
  "id": 1
}

Not Found: 404 Not Found

json
{
  "error": "Todo not found"
}

Example:

typescript
const response = await fetch("/api/todos/1", {
  method: "DELETE",
});

const result = await response.json();
// { success: true, id: 1 }


#### POST /api/todos/reorder

Batch update todo orders (for drag-and-drop).

Authentication: Required

Request Body:

json
{
  "updates": [
    { "id": 1, "order": 2 },
    { "id": 2, "order": 0 },
    { "id": 3, "order": 1 }
  ]
}

Response: 200 OK

json
{
  "success": true
}

Validation Errors: 400 Bad Request

json
{
  "error": "Updates must be an array"
}

Authorization Error: 403 Forbidden

json
{
  "error": "One or more todos not found or unauthorized"
}

Example:

typescript
const response = await fetch("/api/todos/reorder", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    updates: [
      { id: 1, order: 2 },
      { id: 2, order: 0 },
      { id: 3, order: 1 },
    ],
  }),
});

const result = await response.json();


🔒 Security

User Scoping

All queries automatically filter by the authenticated user's ID:

typescript
// Users can only access their own todos
const todos = await db
  .select()
  .from(todos)
  .where(eq(todos.userId, user.id));

SQL Injection Prevention

Drizzle ORM provides parameterized queries, preventing SQL injection:

typescript
// ✅ Safe - Parameterized
db.select().from(todos).where(eq(todos.id, id));

// ❌ Never do this
db.execute(`SELECT * FROM todos WHERE id = ${id}`);

CSRF Protection

NextAuth provides CSRF protection via:

  • Secure, HttpOnly cookies
  • CSRF tokens in requests
  • SameSite cookie attributes

📊 Response Codes

CodeMeaning
200Success
201Created
400Bad Request (validation error)
401Unauthorized (not logged in)
403Forbidden (not your resource)
404Not Found
500Internal Server Error

🧪 Testing APIs

Using curl

bash
# Note: Must be authenticated (use browser session)

# Get all todos
curl http://localhost:3000/api/todos

# Create todo
curl -X POST http://localhost:3000/api/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"Test task","comment":"Test comment"}'

# Update todo
curl -X PATCH http://localhost:3000/api/todos/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Updated task"}'

# Delete todo
curl -X DELETE http://localhost:3000/api/todos/1

Using Playwright

typescript
test("GET /api/todos requires auth", async ({ request }) => {
  const response = await request.get("/api/todos");
  expect(response.status()).toBe(401);
});

Using Vitest

typescript
it("validates todo title", async () => {
  const mockRequest = {
    json: async () => ({ title: "" }),
  };

  const response = await POST(mockRequest as any);
  const data = await response.json();

  expect(response.status).toBe(400);
  expect(data.error).toContain("Title is required");
});

🚀 Rate Limiting (Future)

Currently no rate limiting. For production, consider:

Example implementation:

typescript
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, "10 s"),
});

export async function GET(request: Request) {
  const identifier = request.headers.get("x-forwarded-for");
  const { success } = await ratelimit.limit(identifier);

  if (!success) {
    return new Response("Too Many Requests", { status: 429 });
  }

  // Handle request
}

📚 TypeScript Types

Todo Type

typescript
export interface Todo {
  id: number;
  userId: string;
  title: string;
  comment: string | null;
  order: number;
  createdAt: Date;
  updatedAt: Date;
}

export interface NewTodo {
  userId: string;
  title: string;
  comment?: string | null;
  order?: number;
}

API Response Types

typescript
// Success responses
type TodosResponse = Todo[];
type TodoResponse = Todo;
type DeleteResponse = { success: true; id: number };
type ReorderResponse = { success: true };

// Error responses
type ErrorResponse = { error: string };

🔗 Related Documentation


API Version: 1.0.0

© 2026 Jonathan Leahy · v1.0.9