AY

CRUD With a Simple "RESTful" Server

JavaScript
Node.js
React

Alex | Last updated: March 18, 2021

“CRUD” is a computer programming acronym standing for the four basic operations of persistent storage:

I wanted to record how to simulate a simple RESTful web server that supports CRUD operations.

RESTful

REST (Representational State Transfer) describes a system for organizing information and scaling web applications. How it is defined, in theory, and how it is used in practice is fairly different. So what I’ll go ahead and do is ignore the theory and talk about generally what it looks like in the field—this is also a helpful approach for stuff like AGILE or whatever buzz acronym you hear in the industry, mainly because things often take a very different appearance in practice than in theory.

The most important aspects of REST in web applications are that:

For instance, if we had an app where users create todos, each “todo” would be represented by a resource and have a unique url slug like so:

/todo/{id}

where todo points to the resource collection containing all todos and ${id} represents a pointer to an individual todo data object with a corresponding id.

The convention I’ll use for this post is to assume all resources of the same type are pooled by the unqiue collection identifier—in this case, todo.

With this organizational structure, we can very simply break down our CRUD operations:

OperationURLHTTP MethodFunction
Read{resource}/GETfetches all resources of a given collection
Read{resource}/{id}GETfetches an individual resource
Create{resource}/POSTcreate a new resource
Delete{resource}/{id}DELETEdelete a the resource with the corresponding id
Update{resource}/{id}PUTreplaces the entire resource with the corresponding id
Update{resource}/{id}PATCHreplaces parts of the resource with the corresponding id

Simple Web Server: TODOS

We are going to create a backend server with for a mock Todo application.

Let’s assume Todos have the following schema:

Todo {
  id: uuidv4,
  content: string,
  complete: boolean,
  due: datetime,
}

In lieu of using Node’s built-in web server module, http, let’s use express, which provides helpful abstractions for building a backend server.

Initalize Server With Express

// Backend server index.js
const express = require('express');

// To allow for cross-origin access
const cors = require('cors');
// To generate ids for resources
const { v4: uuidv4 } = require('uuid');

// Intialize backend server
const app = express();

// Allow cross-origin access (for example only, this is insecure practice)
app.use(cors());
// Activate the json-parser
app.use(express.json());
// Make express show static content, index.html, from production dist folder
app.use(express.static('dist'));

// Initialize data array for our todo resources
let todos = [
  ...
];

// TODO: CRUD OPERATION ROUTES/DEFINITIONS HERE

// Save our PORT location
const PORT = process.env.PORT;

// Have our app listen to the running PORT so we can respond to requests
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`)
})

Awesome! We now have everything we need to build our backend application.

For the backend, we need to create a REST interface for operating on todos, and we can do this by creating routes for our todos.

Read

/**
 * Route for getting all todo resources
*/
app.get('/api/todos', (reuest, response) => {
  // return JSON-formatted string with Content-Type header as application/json
  response.json(todos)
})

/**
 * Route for getting individual todo resource by id
*/
app.get('/api/todos/:id', (request, response) => {
  const id = request.params.id;
  const todo = todos.find((todo) => {
    return todo.id === id
  })
  if (note) {
    response.json(note);
  } else {
    response.status(404).end();
  }
})

Create

app.post('/api/todos', (request, response) => {
  const {body} = request;
  if (!body.content) {
    return response.status(400).json({
      error: 'content missing'
    })
  }
  if (!body.due) {
    return response.status(400).json({
      error: 'due date missing'
    })
  }

  // It's generally best practice to generate ids in the backend
  const todo = {
    content: body.content,
    due: body.due,
    complete: false,
    id: uuidv4(),
  }

  // Update our todos list
  todos = todos.concat(todo);

  // Send the resultant todo in the response
  response.json(todo)
})

Update

/**
 * Route for updating an individual todo resource by id
*/
app.put('/api/todos/:id', (request, response) => {
    const { body } = request;
  if (!body.content) {
    return response.status(400).json({
      error: 'content missing'
    })
  }

  todos = todos.map((todo) => {
    if (todo.id == request.params.id) {
      return {... todo, content: body.content}
    } else {
      return todo
    }
  })
})

Delete

/**
 * Route for deleting todo resource by id
*/
app.delete('/api/todos/:id', (request, response) => {
  const id = requests.params.id;
  todos = todos.filter(todo => todo.id !== id);
  
  // Status code 204 indicates no content
  response.status(204).end()
})

Requesting from Frontend

Now that we’ve completed our backend interface, with routes defined to respond to client requests, how do we define our CRUD operations on the frontend?

We build a module responsible for communicating with the backend!

Fetch

The Fetch API provides global functionality allowing us to make HTTP requests. Specifically, it allows us to hse the global fetch method, which allows us to asynchronously fetch resources from the network and returns promises.

This code will be in our front-end code! It will be how our application hits the backend—and the backend will respond according to the previous route definitions.

Let’s start by defining our base url:

const BASE_SERVER_URL = "https://[secure_url_pointing_to_todos]/api/todos"

At the very end, we will also export the following:

export default {
  getById: getById,
  getAll: getAll,
  create: create,
  updateById: updateById,
  deleteById: deleteById
}

Let’s define these functions.

Reading With Fetch

/**
 * Get a specific resource from our api URL
*/
async function getById(id, url=BASE_SERVER_URL) {
  const response = await fetch(`${url}/${id}`, {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
  },
})
return response.json()
}

/**
 * Get all resources from our api URL
*/
async function getAll(url=BASE_SERVER_URL) {
  const response = await fetch(url, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
  })
  return response.json()
}

Creating With Fetch

/**
 * Create a new resource at our api URL
*/
async function create(data, url=BASE_SERVER_URL) {
  // We let the server generate an id for our resources
// todoObject follows a Todo Object Schema
    const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(data)
  })
  return response.json()
}

Updating With Fetch

/**
 * Update an existing resource based on id at our api URL
*/
async function updateById(id, updatedTodo, url=BASE_SERVER_URL) {
  // update
    const response = await fetch(url, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(data)
  })
  return response.json()
}

Deleting With Fetch

/**
 * Delete a resource with the provided id at our api URL
*/
async function deleteById(id, url=BASE_SERVER_URL) {
    const response = await fetch(url, {
    method: "DELETE",
  })
}

Using Axios

Axios is a popular tool for making HTTP requests, and many people prefer it over fetch because:

So let’s use some axios!

An Aside: Promise Chaining and Event Handlers

I’ll be using promise chaining with axios. One can also promise chain using fetch (instead of async/await), but I decided to use different syntax for both.

For promise chaining with axios, we can access the result of an operation returned by an axios promise (e.g. a promise returned by axios.get()) using an event handler tied to the .then method.

Reading With Axios

// Frontend interface with backend using axios
import axios from 'axios';

function getById(id, url=BASE_SERVER_URL) {
  return axios
    .get(`${url}/${id}`)
    .then((response) => {
      // Return all todos
      return response.data;
    })
    .catch((error) => {
      console.log(error);
    })
}

function getAll(url=BASE_SERVER_URL) {
  return axios
    .get(url)
    .then((response) => {
      // Return all todos
      return response.data;
    })
    .catch((error) => {
      console.log(error);
    })
}

Creating With Axios

function create(newTodo, url=BASE_SERVER_URL) {
  // We let the server generate an id for our resources
// todoObject follows a Todo Object Schema
  return axios
    .post(url, todoObject)
    .then((response) => {
      // Return the new todo
      return response.data;
    })
    .catch((error) => {
      console.log(error);
    })
}

Updating With Axios

function updateById(id, updatedTodo, url=BASE_SERVER_URL) {
  // update
  return axios
    .put(`${url}/${id}`, updatedTodo)
    .then((response) => {
      // Return the updated todo
      return response.data;
    })
    .catch((error) => {
      console.log(error);
    })
}

Deleting With Axios

function deleteById(id, url=BASE_SERVER_URL) {
  return axios
    .delete(`${url}/${id}`)
    .catch((error) => {
      console.log(error);
    })
}

Using the API With React Hooks

We call the API in effect hooks, often in tandem with state hooks.

A common pattern is to initialize a state to an empty version of whatever data structure we need to store the data we fetch from the server, and then fetch our data from the server within the useEffect hook and set the data using the event handler of the fetch operation:

[data, setData] = useState([]);

useEffect(() => {
  fetchData().then(({data}) => {
    setData(data);
  })
}, [])

In React applications, you will often find yourself setting state that captures your data as part of the event handlers once your async promises have returned.