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
{
"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
[
{
"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
{
"error": "Unauthorized"
}Example:
const response = await fetch("/api/todos");
const todos = await response.json();
#### POST /api/todos
Create a new todo.
Authentication: Required
Request Body:
{
"title": "Task name",
"comment": "Optional comment" // Optional
}
Response: 201 Created
{
"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
{
"error": "Title is required and must be a string"
}Example:
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
{
"title": "Updated title",
"comment": "Updated comment",
"order": 5
}
Response: 200 OK
{
"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
{
"error": "Title must be a non-empty string"
}
Not Found: 404 Not Found
{
"error": "Todo not found"
}Example:
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
200 OK
{
"success": true,
"id": 1
}
Not Found: 404 Not Found
{
"error": "Todo not found"
}Example:
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:
{
"updates": [
{ "id": 1, "order": 2 },
{ "id": 2, "order": 0 },
{ "id": 3, "order": 1 }
]
}
Response: 200 OK
{
"success": true
}
Validation Errors: 400 Bad Request
{
"error": "Updates must be an array"
}
Authorization Error: 403 Forbidden
{
"error": "One or more todos not found or unauthorized"
}Example:
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:
// 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:
// ✅ 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
| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Bad Request (validation error) |
| 401 | Unauthorized (not logged in) |
| 403 | Forbidden (not your resource) |
| 404 | Not Found |
| 500 | Internal Server Error |
🧪 Testing APIs
Using curl
# 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
test("GET /api/todos requires auth", async ({ request }) => {
const response = await request.get("/api/todos");
expect(response.status()).toBe(401);
});
Using Vitest
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:
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
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
// 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