Skip to content

The JsonPlaceholder-backed TodoApi ​

The definition ​

NOTE

Why is it so useful to define the API instead of implementing it directly?

Because I can have multiple implementations of the same concept. we will use the fetch implementation for production and the in-memory one for tests.

ts
// src/spa-client-side/setup/TodoApi.ts

export interface Todo {
  userId: number
  id: number
  title: string
  completed: boolean
}

export interface TodoApi {
  getTodos: () => Promise<Todo[]>
  getTodo: (id: number) => Promise<Todo>
  patchTodo: (
    id: number,
    data: Partial<Pick<Todo, 'title' | 'completed'>>,
  ) => Promise<Todo>
  deleteTodo: (id: number) => Promise<void>
}
Fetch implementation

For the fetch implementation, we will add a global delay to simulate a network delay and have time to observe loading states.

ts
// src/spa-client-side/setup/TodoApi.fetch.ts

import { delayApiCall } from './api-latency'
import { TodoApi } from './TodoApi'

export const todoFetchApi: TodoApi = {
  async getTodo(id) {
    await delayApiCall()
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/todos/${id}`,
    )
    return response.json()
  },

  async getTodos() {
    await delayApiCall()
    const response = await fetch('https://jsonplaceholder.typicode.com/todos')
    return response.json()
  },

  async patchTodo(id, data) {
    await delayApiCall()
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/todos/${id}`,
      {
        method: 'PATCH',
        body: JSON.stringify(data),
        headers: { 'Content-type': 'application/json; charset=UTF-8' },
      },
    )
    return response.json()
  },

  async deleteTodo(id) {
    await delayApiCall()
    await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
      method: 'DELETE',
    })
  },
}
In-memory implementation
ts
// src/spa-client-side/setup/TodoApi.InMemory.ts

import { TodoApi, Todo } from './TodoApi'

interface TodoInMemoryApi extends TodoApi {
  createTodo: (todo: Todo) => void
}

export function makeTodoInMemoryApi(): TodoInMemoryApi {
  const store = new Map<number, Todo>()
  return {
    createTodo(todo: Todo) {
      store.set(todo.id, todo)
    },
    async getTodo(id) {
      const todo = store.get(id)
      if (!todo) throw new Error(`todo ${id} not found`)
      return todo
    },

    async getTodos() {
      return Array.from(store.values())
    },

    async patchTodo(id, data) {
      const nextTodo = { ...(await this.getTodo(id)), ...data }
      store.set(id, nextTodo)
      return nextTodo
    },

    async deleteTodo(id) {
      store.delete(id)
    },
  }
}

Now that we have the API, let’s dig into the remote data concept to use the API.