Advanced Guide to HTTP CRUD Operations Using Fetch API in Remix.js with TypeScript

Ram Kumar

Ram Kumar

January 18, 20254 min read

Advanced Guide to HTTP CRUD Operations Using Fetch API in Remix.js with TypeScript

In this advanced guide, we will walk through performing HTTP CRUD operations in a Remix.js application using the Fetch API and TypeScript. Remix.js is a modern framework for building fast, scalable web applications. It leverages server-side rendering (SSR), client-side hydration, and advanced caching strategies to deliver the best user experience. One of the core requirements for many applications is interacting with a backend API to perform CRUD operations—Create, Read, Update, and Delete. We'll explore how to implement these operations in Remix using TypeScript.

Prerequisites

Before we dive into the code, let's ensure you have the following:

Remix.js application up and running.

TypeScript enabled in your Remix project.

Basic knowledge of HTTP methods (GET, POST, PUT, DELETE).

Understanding of how to interact with REST APIs or any backend service.

If you haven’t set up a Remix project, you can quickly create one by following the Remix Documentation.

Step 1: Setting Up the TypeScript Types

One of the major benefits of using TypeScript in your Remix application is that it enforces strict types, which help prevent runtime errors and ensure better developer experience.

To begin, let’s define TypeScript types for the data we will be working with. For simplicity, let’s assume we are interacting with a task management API, where each task has an ID, title, description, and status.

Create a types.ts file in the src folder to define the types.

// src/types.ts

export interface Task {
  id: number;
  title: string;
  description: string;
  status: "pending" | "in-progress" | "completed";
}

This simple interface represents the shape of the task object.

Step 2: Define the Fetch API Wrapper

Next, we’ll create a utility file for interacting with the backend API using the Fetch API. This will allow us to abstract away the logic for each CRUD operation, making our codebase cleaner and easier to maintain.

Create a new file src/utils/api.ts:

// src/utils/api.ts
import { Task } from "~/types";

const API_URL = "https://example.com/api/tasks"; // Your API endpoint

// Helper function to handle fetch requests
const fetchAPI = async (url: string, options: RequestInit = {}) => {
  const response = await fetch(url, options);
  if (!response.ok) {
    throw new Error(`Failed to fetch: ${response.statusText}`);
  }
  return response.json();
};

// Create Task
export const createTask = async (task: Omit<Task, 'id'>): Promise<Task> => {
  const response = await fetchAPI(API_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(task),
  });
  return response;
};

// Get All Tasks
export const getTasks = async (): Promise<Task[]> => {
  return fetchAPI(API_URL);
};

// Get Task by ID
export const getTaskById = async (id: number): Promise<Task> => {
  return fetchAPI(`${API_URL}/${id}`);
};

// Update Task
export const updateTask = async (id: number, task: Partial<Task>): Promise<Task> => {
  const response = await fetchAPI(`${API_URL}/${id}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(task),
  });
  return response;
};

// Delete Task
export const deleteTask = async (id: number): Promise<void> => {
  await fetchAPI(`${API_URL}/${id}`, {
    method: "DELETE",
  });
};

In this file, we define functions for each CRUD operation:

  • createTask: Sends a POST request to create a new task.
  • getTasks: Sends a GET request to fetch all tasks.
  • getTaskById: Sends a GET request to fetch a specific task by its ID.
  • updateTask: Sends a PUT request to update a task.
  • deleteTask: Sends a DELETE request to remove a task.

Step 3: Using Fetch API in Remix Loader Functions

In Remix.js, we can use loader functions to fetch data on the server before rendering the page. These loader functions run on the server when the route is requested, allowing us to fetch and pass data to the component as props.

Let’s create a loader to fetch tasks in a tasks route.

// src/routes/tasks.tsx
import { json, LoaderFunction } from "remix";
import { getTasks } from "~/utils/api";
import { Task } from "~/types";

export let loader: LoaderFunction = async () => {
  const tasks: Task[] = await getTasks();
  return json({ tasks });
};

export default function Tasks() {
  return (
    <div>
      <h1>Task List</h1>
      {/* Render tasks here */}
    </div>
  );
}

Here, the loader fetches the tasks and returns them as JSON. We can then use the data in the component by passing it to the page through Remix’s useLoaderData hook.

// src/routes/tasks.tsx
import { useLoaderData } from "remix";
import { Task } from "~/types";

export default function Tasks() {
  const { tasks } = useLoaderData<{ tasks: Task[] }>();

  return (
    <div>
      <h1>Task List</h1>
      <ul>
        {tasks.map((task) => (
          <li key={task.id}>
            <h3>{task.title}</h3>
            <p>{task.description}</p>
            <span>{task.status}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

Step 4: Handling CRUD Operations on the Client Side

On the client side, we can perform CRUD operations using form submissions and buttons. Let’s add the ability to create a new task, update an existing task, and delete a task.

// src/routes/tasks.tsx
import { useState } from "react";
import { useLoaderData } from "remix";
import { Task } from "~/types";
import { createTask, updateTask, deleteTask } from "~/utils/api";

export default function Tasks() {
  const { tasks } = useLoaderData<{ tasks: Task[] }>();
  const [newTask, setNewTask] = useState<Omit<Task, "id">>({
    title: "",
    description: "",
    status: "pending",
  });

  const handleCreate = async () => {
    const createdTask = await createTask(newTask);
    // Optionally, update the state or re-fetch the data
  };

  const handleDelete = async (id: number) => {
    await deleteTask(id);
    // Optionally, update the state or re-fetch the data
  };

  const handleUpdate = async (id: number, updatedTask: Partial<Task>) => {
    const updated = await updateTask(id, updatedTask);
    // Optionally, update the state or re-fetch the data
  };

  return (
    <div>
      <h1>Task List</h1>
      <div>
        <h2>Create a New Task</h2>
        <input
          type="text"
          placeholder="Title"
          value={newTask.title}
          onChange={(e) => setNewTask({ ...newTask, title: e.target.value })}
        />
        <textarea
          placeholder="Description"
          value={newTask.description}
          onChange={(e) => setNewTask({ ...newTask, description: e.target.value })}
        />
        <select
          value={newTask.status}
          onChange={(e) => setNewTask({ ...newTask, status: e.target.value as "pending" | "in-progress" | "completed" })}
        >
          <option value="pending">Pending</option>
          <option value="in-progress">In Progress</option>
          <option value="completed">Completed</option>
        </select>
        <button onClick={handleCreate}>Create Task</button>
      </div>

      <ul>
        {tasks.map((task) => (
          <li key={task.id}>
            <h3>{task.title}</h3>
            <p>{task.description}</p>
            <span>{task.status}</span>
            <button onClick={() => handleDelete(task.id)}>Delete</button>
            <button onClick={() => handleUpdate(task.id, { status: "completed" })}>Mark as Completed</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Step 5: Error Handling and Optimization

For a production-level application, you’ll need to implement proper error handling and UI feedback for each CRUD operation.

  • Error Handling: Add try-catch blocks around async operations to handle potential errors and display error messages.
  • Optimistic UI Updates: For a better user experience, consider updating the UI optimistically (without waiting for the API response) and reverting changes if the operation fails.
  • State Management: You can use a state management library like Redux or React Query to manage and sync the state of tasks more efficiently.

Conclusion

In this guide, we’ve explored how to perform CRUD operations using the Fetch API in a Remix.js application with TypeScript. We demonstrated the process of defining types, creating reusable API functions, and integrating them into loader functions and components. With the Fetch API, Remix, and TypeScript, you can easily manage HTTP requests while keeping your application clean, type-safe, and maintainable.

Previous: Creating Dynamic Forms in Angular 19 with Reactive Forms Module
Next: Best Practices for Building Applications with RemixJS and TypeScript