LogoBoring Template

API endpoints

Overview of the RESTful API endpoints and how to use them in your application.

This documentation provides a comprehensive overview of the RESTful API endpoints available in the application and how to use them with our type-safe API client.

Available Endpoints

Items API

  • GET, POST, DELETE - /api/items - Manage items for workspaces

  • GET, PUT, DELETE - /api/items/:id - Manage individual items

User API

  • GET - /api/users - Retrieve user information

  • GET, PUT, DELETE - /api/users/:id - Manage individual users

Workspace API

  • GET, POST - /api/workspaces - Manage workspaces

  • GET, PUT, DELETE - /api/workspaces/:id - Manage individual workspaces

Other Endpoints

  • POST - /api/feedback - Submit user feedback

Type-Safe API Client

The application provides a type-safe API client to simplify API requests and ensure type safety across the codebase. The client is implemented in lib/api-client.ts.

API Client Implementation

lib/api-client.ts
import { ItemType, UserType, WorkspaceType } from "@/server/db/schema-types";
import { z } from "zod";
 
import {
  createItemSchema,
  feedbackSchema,
  updateWorkspaceSchema,
  userSchema,
  workspaceSchema,
} from "@/lib/schemas";
 
// Type-safe API client
export class ApiClient {
  private static async fetch<T>({
    endpoint,
    ...config
  }: {
    endpoint: string;
    method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
    body?: unknown;
  }): Promise<T> {
    const res = await fetch(`/api${endpoint}`, {
      ...config,
      headers: {
        "Content-Type": "application/json",
      },
      body: config.body ? JSON.stringify(config.body) : undefined,
    });
 
    const data = await res.json();
 
    if (!res.ok) {
      throw new Error(data.error || "An error occurred");
    }
 
    return data as T;
  }
 
  static async updateUser(data: UpdateUserRequest): Promise<ApiResponse> {
    return this.fetch({
      endpoint: `/users/${data.id}`,
      method: "PATCH",
      body: data.values,
    });
  }
 
  static async deleteUser(id: UserType["id"]): Promise<DeleteUserResponse> {
    return this.fetch({
      endpoint: `/users/${id}`,
      method: "DELETE",
    });
  }
 
  static async createItem(data: CreateItemRequest): Promise<ApiResponse> {
    return this.fetch({
      endpoint: "/items",
      method: "POST",
      body: data,
    });
  }
 
  static async deleteItem(id: ItemType["id"]): Promise<ApiResponse> {
    return this.fetch({
      endpoint: `/items/${id}`,
      method: "DELETE",
    });
  }
 
  static async getItems(
    slug?: WorkspaceType["slug"]
  ): Promise<{ data: ItemWithCreator[] }> {
    return this.fetch({
      endpoint: `/items?slug=${slug}`,
      method: "GET",
    });
  }
 
  static async createWorkspace(
    data: CreateWorkspaceRequest
  ): Promise<CreateWorkspaceResponse> {
    return this.fetch({
      endpoint: "/workspaces",
      method: "POST",
      body: data,
    });
  }
 
  static async updateWorkspace(
    data: UpdateWorkspaceRequest
  ): Promise<UpdateWorkspaceResponse> {
    return this.fetch({
      endpoint: `/workspaces/${data.id}`,
      method: "PATCH",
      body: data.values,
    });
  }
 
  static async deleteWorkspace(
    id: WorkspaceType["id"]
  ): Promise<DeleteWorkspaceResponse> {
    return this.fetch({
      endpoint: `/workspaces/${id}`,
      method: "DELETE",
    });
  }
 
  static async sendFeedback(
    data: z.infer<typeof feedbackSchema>
  ): Promise<ApiResponseWithDescription> {
    return this.fetch({
      endpoint: "/feedback",
      method: "POST",
      body: data,
    });
  }
}

Type Definitions

lib/api-client-types.ts
interface ApiResponse {
  message: string;
}
 
interface ApiResponseWithDescription extends ApiResponse {
  description: string;
}
 
interface CreateWorkspaceResponse extends ApiResponse {
  slug: string;
  workspace: Pick<WorkspaceType, "id" | "slug" | "name" | "logo" | "createdAt">;
}
 
interface DeleteWorkspaceResponse extends ApiResponse {
  description: string;
  redirectUrl: string;
}
 
interface DeleteUserResponse extends ApiResponse {
  description: string;
}
 
interface UpdateWorkspaceResponse extends ApiResponse {
  newSlug: string | null;
  updatedWorkspace: Pick<
    WorkspaceType,
    "id" | "slug" | "name" | "logo" | "updatedAt"
  >;
}
 
interface ItemWithCreator extends ItemType {
  creator: {
    name: string | null;
    email: string;
    image: string | null;
  };
}
 
export interface UpdateUserRequest {
  id: UserType["id"];
  values: z.infer<typeof userSchema>;
}
 
export interface CreateItemRequest {
  slug: WorkspaceType["slug"];
  values: z.infer<typeof createItemSchema>;
}
 
export interface UpdateWorkspaceRequest {
  id: WorkspaceType["id"];
  values: z.infer<typeof updateWorkspaceSchema>;
}
 
export interface DeleteItemRequest {
  id: ItemType["id"];
}
 
export interface CreateWorkspaceRequest {
  values: z.infer<typeof workspaceSchema>;
  isInitial: boolean;
}

Using the API Client

Basic Usage

Example of using the API client to fetch items:

Example: Basic API client usage
import { ApiClient } from "@/lib/api-client";
 
// Get all items for a workspace
async function getWorkspaceItems(workspaceSlug: string) {
  try {
    const { data } = await ApiClient.getItems(workspaceSlug);
    return data;
  } catch (error) {
    console.error("Failed to fetch items:", error);
    return [];
  }
}
 
// Create a new item
async function createNewItem(
  workspaceSlug: string,
  itemData: CreateItemRequest
) {
  try {
    const response = await ApiClient.createItem({
      slug: workspaceSlug,
      values: itemData,
    });
    return response;
  } catch (error) {
    console.error("Failed to create item:", error);
    throw error;
  }
}

React Query Integration

Example of using the API client with React Query:

Example: React Query integration
import { ApiClient } from "@/lib/api-client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
 
// Query hook for fetching items
export function useWorkspaceItems(workspaceSlug: string) {
  return useQuery({
    queryKey: ["items", workspaceSlug],
    queryFn: () => ApiClient.getItems(workspaceSlug),
  });
}
 
// Mutation hook for creating items
export function useCreateItem() {
  const queryClient = useQueryClient();
 
  return useMutation({
    mutationFn: ApiClient.createItem,
    onSuccess: (_, variables) => {
      queryClient.invalidateQueries({ queryKey: ["items", variables.slug] });
    },
  });
}

Authentication & Authorization

All API endpoints require authentication through Auth.js sessions. The server retrieves the current user using the getCurrentUser() function.

Authentication Flow

API route authentication
// 1. Retrieve the current user
const { user } = await getCurrentUser();
 
// 2. Check if the user exists
if (!user) {
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
 
// 3. Proceed with authenticated operations
// ...

Rate Limiting

All API endpoints implement rate limiting using Upstash Ratelimit to prevent abuse. Rate limits are defined in the constants.ts file.

Rate Limit Implementation

Example: Rate limiting implementation
// Initialize rate limiter
const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(RATE_LIMIT_10, RATE_LIMIT_1_MINUTE),
});
 
// Apply rate limiting
const identifier = `ratelimit:action-name:${user.id}`;
const { success } = await ratelimit.limit(identifier);
 
if (!success) {
  return NextResponse.json({ error: "Rate limit exceeded" }, { status: 429 });
}

Working with Items

Retrieving Items

Get all items or filter by workspace slug:

Example: Retrieving items
// GET /api/items?slug=workspace-slug
const response = await fetch("/api/items?slug=workspace-slug");
const { data } = await response.json();
 
// Response format
{
  data: [
    {
      id: "item-id",
      name: "Item Name",
      description: "Item Description",
      status: "todo",
      tags: ["important", "urgent"],
      dueDate: "2023-06-15T00:00:00.000Z",
      creator: {
        name: "User Name",
        email: "user@example.com",
        image: "https://example.com/avatar.jpg",
      },
      workspaceId: "workspace-id",
      createdAt: "2023-06-01T00:00:00.000Z",
      updatedAt: "2023-06-01T00:00:00.000Z",
    },
    // ...more items
  ];
}

Creating Items

Create a new item in a workspace:

Example: Creating items
// POST /api/items
const response = await fetch("/api/items", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    slug: "workspace-slug",
    values: {
      name: "New Item",
      description: "Item description",
      status: "todo",
      tags: ["important"],
      dueDate: "2023-06-15T00:00:00.000Z",
    },
  }),
});
 
// Response format
{
  message: "Item created successfully";
}

Deleting Items

Delete all items in a workspace (requires DELETE_WORKSPACE permission):

Example: Deleting items
// DELETE /api/items
const response = await fetch("/api/items", {
  method: "DELETE",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    slug: "workspace-slug",
  }),
});
 
// Response format
{
  message: "All items work workspace deleted successfully";
}

Error Handling

The API consistently handles errors and returns appropriate status codes and error messages.

400 - Validation Error

Input data failed validation

{
  "error": "Validation Error",
  "details": [
    {
      "code": "invalid_type",
      "path": ["values", "name"],
      "message": "Required"
    }
  ]
}

401 - Unauthorized

User is not authenticated or lacks permission

{
  "error": "Unauthorized"
}

404 - Not Found

Requested resource does not exist

{
  "error": "Workspace not found"
}

429 - Rate Limit Exceeded

Too many requests in a short period

{
  "error": "Rate limit exceeded"
}

500 - Internal Server Error

An unexpected error occurred on the server

{
  "error": "Internal Server Error",
  "details": "Error message"
}

Permissions System

API endpoints check permissions using the hasPermission utility function. For more information on permissions, see the Role-Based Access Control documentation.

Permission Check Example

Example: Permission checking
// Check if user has permission
const canDelete = await hasPermission(
  user.id,
  workspace.id,
  PERMISSIONS.DELETE_WORKSPACE
);
 
if (!canDelete) {
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

Best Practices

Request Handling

  • Always validate input data with Zod
  • Transform dates properly from strings
  • Log important events for debugging
  • Use rate limiting for all endpoints

Response Formatting

  • Return consistent JSON structures
  • Use appropriate HTTP status codes
  • Include detailed error messages
  • Use specific success messages

Security

  • Always check authentication for protected routes
  • Validate permissions for each operation
  • Sanitize user inputs and validate against schemas
  • Implement appropriate rate limiting

Data Access

  • Use parameterized queries to prevent SQL injection
  • Always check if resources exist before operations
  • Properly scope data access to authorized workspace
  • Use transactions for related operations

Client Integration

  • Use the ApiClient for type-safe requests
  • Handle errors consistently
  • Combine with React Query for data fetching
  • Provide proper loading and error states

Additional Resources

Role-Based Access Control

See the Role-Based Access Control documentation for more information on permissions.

Rate Limiting

Learn about Rate Limiting implementation in the application.