Next, we'll add the ability to mark and unmark tasks as done.
Updates an existing Task by replacing all of its fields with the data from the request body, then returns the updated Task. If no Task exists with the ID in the URL, returns a 404 error. If the ID in the request body doesn't match the ID in the URL or the request body is not a valid Task, returns a 400 error. These errors can be checked in any order.
Example Task in database:
{
"_id": "64ebc766fa1e11e2d987a8e9",
"title": "Part 1.2 backend",
"description": "Implement the PUT /api/task/:id route",
"isChecked": false,
"dateCreated": "2023-09-02T01:49:32.621Z"
}
Example request:
PUT /api/task/64ebc766fa1e11e2d987a8e9
{
"_id": "64ebc766fa1e11e2d987a8e9",
"title": "Part 1.2 backend update",
"description": "Implement the PUT /api/task/:id route update",
"isChecked": true,
"dateCreated": "2023-09-02T01:49:32.621Z"
}
Example response:
200
{
"_id": "64ebc766fa1e11e2d987a8e9",
"title": "Part 1.2 backend update",
"description": "Implement the PUT /api/task/:id route update",
"isChecked": true,
"dateCreated": "2023-09-02T01:49:32.621Z"
}
🤔 For new developers: POST and PUT
Note that we use POST
requests to create Tasks and PUT
requests to update them. This isn't strictly necessary (we could, say, use POST
for everything), but it's a standard industry practice because it follows the original HTTP specification more closely. The main difference between the two methods is "idempotence"—PUT
actions are expected to be idempotent, while POST
actions are not.
✅ Good practice: Limit user-modifiable fields
Here we just replace the entire Task object with the provided data, even the dateCreated
field. We do this for simplicity, but in a real project, it might be advisable to limit which fields can be modified by a user request.
- Copy the skeleton code below into
backend/src/controllers/task.ts
.export const updateTask: RequestHandler = async (req, res, next) => { // your code here try { // your code here } catch (error) { next(error); } };
- Use the
validationResult
andvalidationErrorParser
functions to validate the request body. See thecreateTask
function in the same file for an example.validationErrorParser
will stop the request and generate a 400 response by itself if the request body doesn't contain valid Task data. - Compare the
:id
from the request URL (req.params.id
; seegetTask
for an example) with the_id
in the request body (req.body._id
). If they're not equal, return a 400 response (just callres.status(400);
). - Use the
Model.findByIdAndUpdate()
Mongoose function to update the Task in the database with the given ID. (Actually, there are several functions you can use; any approach that works is valid.)- Remember to
await
the returnedQuery
. - If the returned
Query
gives us null, then there was no object in the database with that ID. In that case, return a 404 response. - Otherwise, return a 200 response containing the updated Task. The result of
findByIdAndUpdate
is the original Task, so you should execute a new query like ingetTask
.
- Remember to
- Add the new route to
src/routes/task.ts
. Similar tocreateTask
, use theupdateTask
validation chain provided insrc/validators/task.ts
. - Test your implementation. Make sure your backend is running locally, then call the new route through Postman or run the following command with your own values filled in:
You should see the Task updated with its new data when you list all Tasks in mongosh and when you view the frontend Home page.
curl -X "PUT" http://127.0.0.1:3001/api/task/<paste a Task ID from your database here> \ -H "Content-Type: application/json" \ -d '{"_id":"<the same ID>","title":"<Your title>","description":"<Your description>","isChecked":false,"dateCreated":"2023-10-01T00:00Z"}'
- Copy the skeleton code below into
frontend/src/api/tasks.ts
:export async function updateTask(task: UpdateTaskRequest): Promise<APIResult<Task>> { try { // your code here } catch (error) { return handleAPIError(error); } }
- Using the existing functions as guides, complete the implementation of
updateTask
.
- When the user presses the
CheckButton
, call theupdateTask
function to flip the value ofisChecked
for thisTaskItem
'sTask
object. - Re-render the
TaskItem
whenupdateTask
resolves to the updatedTask
.- If an error occurs, then just
alert()
the user.
- If an error occurs, then just
- Prevent the user from pressing the
CheckButton
again untilupdateTask
has resolved (this will require at least one additional state variable).
-
In
frontend/src/components/TaskItem.tsx
, add new state variablestask
andisLoading
and a helper functionhandleToggleCheck
:import React, { useState } from "react"; // update this line // ... export function TaskItem({ task: initialTask }: TaskItemProps) { // update the previous line and add the following const [task, setTask] = useState<Task>(initialTask); const [isLoading, setLoading] = useState<boolean>(false); const handleToggleCheck = () => { // your code here }; // ... }
-
Within
handleToggleCheck
, setisLoading
to true (by callingsetLoading
—never assign directly to a state variable, because React will ignore it), then call theupdateTask
function (be sure to import it fromsrc/api/tasks
). Pass in thetask
state variable, with the value ofisChecked
flipped.🤔 For new developers: Spread syntax
An easy way to do this is to use JavaScript's spread syntax. You can write something like
{ ...task, isChecked: !task.isChecked }
. This is preferable because it's concise and it creates a (shallow) copy oftask
; we shouldn't modifytask
or any other props directly because that might cause unintended side effects. -
When
updateTask
resolves, callsetTask
with the new task from the response (oralert()
the user again if it failed) and setisLoading
back to false. See thehandleSubmit
function incomponents/TaskForm.tsx
for an example of how to handle the result of a request (the request iscreateTask
in that case).🤔 For new developers: await or async
If you make
handleToggleCheck
anasync
function, you can useawait
syntax instead ofthen()
. There's no real difference here, so it's up to preference. Just stay consistent and don't mix the two syntaxes together.❓ Hint: Where to call setLoading
Make sure you call
setLoading(false)
inside the.then()
block. If it's outside, then it will run immediately instead of after the response-handling code finishes. -
Pass two props to the
CheckButton
:onPress
anddisabled
. We wanthandleToggleCheck
to be called when theCheckButton
is pressed, and we want theCheckButton
to be disabled whenisLoading
is true.✅ Good practice: Organize helper functions together
We could write the
onPress
handler function directly inside the JSX instead of storing it in a named variable. However, we'd recommend only doing that for super simple, one-line functions. Otherwise, it's generally a good practice to organize nontrivial helper functions likehandleToggleCheck
separately from our rendering code, to make them easier to find. -
Test your changes by checking and unchecking some tasks on the Home page. Make sure that each
TaskItem
updates its text color correctly and that after clicking theCheckButton
, you can't click it again until theTaskItem
has updated. To be extra sure, you can verify that the value ofisChecked
changes in mongosh. Also make sure it works as expected when you shut down your local backend (to simulate network unavailability).
Follow the same process as in Part 1.1: stage your changes using git add
, commit them using git commit
with a descriptive message, and push them using git push
.
Previous | Up | Next |
---|---|---|
1.1. Implement the task list | Part 1 | 1.3. Make a pull request |