CRUD With a Simple "RESTful" Server
Alex | Last updated: March 18, 2021
“CRUD” is a computer programming acronym standing for the four basic operations of persistent storage:
- Create
- Read
- Update
- Delete
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:
- Data objects in web applications are called “resources” in RESTful thinking
- Each “resource” has a unique, associated address (URL)
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:
Operation | URL | HTTP Method | Function |
---|---|---|---|
Read | {resource}/ | GET | fetches all resource s of a given collection |
Read | {resource}/{id} | GET | fetches an individual resource |
Create | {resource}/ | POST | create a new resource |
Delete | {resource}/{id} | DELETE | delete a the resource with the corresponding id |
Update | {resource}/{id} | PUT | replaces the entire resource with the corresponding id |
Update | {resource}/{id} | PATCH | replaces 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:
- Axios existed before fetch, and so brings with it better backwards compatability—it’s also seldom worth replacing key dependencies such as Axios once it’s so engrained within a project.
- Axios has many nice quality of life behaviors, such as auto-parsing JSON and automatically throwing errors with error (400/500) response codes.
- Axios handles progress tracking elegantly.
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.