Weekend Project: Implement Basic Servers in Node.js, Python, and PHP

A hands-on comparison of backend technologies using structured problem solving

Introduction to the Weekend Project

This weekend project will guide you through implementing a basic REST API server in three popular backend technologies: Node.js, Python, and PHP. By building the same API endpoints across these different environments, you'll gain a deeper understanding of their similarities, differences, and unique characteristics.

Throughout this project, we'll apply George Polya's famous 4-step problem-solving procedure, which provides a systematic approach to tackling complex problems:

flowchart TD A[George Polya's Problem Solving Steps] --> B[1. Understand
the Problem] A --> C[2. Devise
a Plan] A --> D[3. Execute
the Plan] A --> E[4. Review/Extend] B --> B1[Define project requirements] B --> B2[Clarify the API spec] C --> C1[Design the architecture] C --> C2[Choose tools and libraries] D --> D1[Implement Node.js server] D --> D2[Implement Python server] D --> D3[Implement PHP server] E --> E1[Test APIs] E --> E2[Evaluate differences] E --> E3[Extend functionality] style A fill:#f5f5f5,stroke:#333,stroke-width:2px style B fill:#dbeafe,stroke:#333,stroke-width:2px style C fill:#c6f6d5,stroke:#333,stroke-width:2px style D fill:#fef3c7,stroke:#333,stroke-width:2px style E fill:#fecdd3,stroke:#333,stroke-width:2px

Project Objectives

Prerequisites

Step 1: Understand the Problem

Following Polya's first step, we need to thoroughly understand what we're trying to accomplish. Let's define the problem in detail.

Project Requirements

We'll be building a simple "Task Manager" API with the following features:

API Specification

Endpoint Method Description Request Body Response
/api/tasks GET Get all tasks None Array of task objects
/api/tasks/:id GET Get a single task by ID None Single task object
/api/tasks POST Create a new task { title, description } Created task object
/api/tasks/:id PUT Update a task { title, description, status } Updated task object
/api/tasks/:id DELETE Delete a task None Success message

Data Model

The Task object will have the following structure:


{
  "id": "string", // Unique identifier (UUID or similar)
  "title": "string", // Task title
  "description": "string", // Task description
  "status": "string", // "pending", "in-progress", or "completed"
  "created_at": "string" // ISO date string
}
        

Understanding Each Technology

Technology Server Framework Key Characteristics
Node.js Express.js
  • JavaScript runtime built on Chrome's V8 engine
  • Non-blocking, event-driven architecture
  • Single-threaded with asynchronous I/O
  • Vast ecosystem with npm packages
Python Flask
  • Simple and lightweight web framework
  • Easy to get started, minimalist approach
  • Excellent for prototyping and small to medium projects
  • WSGI compatible
PHP Plain PHP / Basic Router
  • Built specifically for web development
  • Simple syntax for handling HTTP requests
  • Wide hosting availability
  • Easy to deploy

Key Questions to Address

Step 2: Devise a Plan

Now that we understand the problem, let's devise a plan for implementing our solution across the three technologies.

High-Level Implementation Plan

  1. Set up development environments for each technology
  2. Implement the Node.js server with Express.js
  3. Implement the Python server with Flask
  4. Implement the PHP server with a basic router
  5. Test each implementation with Postman
  6. Compare and contrast the implementations

Common Components

Despite the differences in languages and frameworks, each implementation will share these common components:

flowchart TB A[API Server] --> B[Route Definitions] A --> C[Request Parsing] A --> D[Data Store] A --> E[Response Formatting] A --> F[Error Handling] B --> B1[GET /api/tasks] B --> B2[GET /api/tasks/:id] B --> B3[POST /api/tasks] B --> B4[PUT /api/tasks/:id] B --> B5[DELETE /api/tasks/:id] style A fill:#f5f5f5,stroke:#333,stroke-width:2px style B fill:#dbeafe,stroke:#333,stroke-width:2px style C fill:#c6f6d5,stroke:#333,stroke-width:2px style D fill:#fef3c7,stroke:#333,stroke-width:2px style E fill:#fecdd3,stroke:#333,stroke-width:2px style F fill:#e0e7ff,stroke:#333,stroke-width:2px

Technology-Specific Plans

Node.js (Express.js) Implementation

  1. Initialize a Node.js project with npm
  2. Install Express.js and other dependencies (uuid, cors, etc.)
  3. Set up the Express application with middleware
  4. Create route handlers for each API endpoint
  5. Implement in-memory storage using a JavaScript array or object
  6. Add validation and error handling
  7. Test the implementation

Python (Flask) Implementation

  1. Create a virtual environment for Python
  2. Install Flask and other dependencies
  3. Initialize the Flask application
  4. Define route handlers using Flask's decorator syntax
  5. Implement in-memory storage using a Python list or dictionary
  6. Add validation and error handling
  7. Test the implementation

PHP Implementation

  1. Set up a basic PHP project structure
  2. Create a simple router to handle different HTTP methods and paths
  3. Implement controller functions for each API endpoint
  4. Use a PHP array for in-memory storage (serialized to a file for persistence)
  5. Add validation and error handling
  6. Configure proper JSON responses
  7. Test the implementation

Project Structure

For organization, we'll use the following directory structure:


weekend-project/
├── nodejs-server/
│   ├── package.json
│   ├── index.js
│   └── ...
├── python-server/
│   ├── app.py
│   ├── requirements.txt
│   └── ...
└── php-server/
    ├── index.php
    ├── router.php
    └── ...
        

Step 3: Execute the Plan

Now we're ready to implement our servers in each technology. We'll start with Node.js, then move to Python, and finally PHP.

Node.js Implementation with Express.js

Setting Up the Project


# Create project directory
mkdir -p weekend-project/nodejs-server
cd weekend-project/nodejs-server

# Initialize npm project
npm init -y

# Install dependencies
npm install express uuid cors

# Create the main file
touch index.js
        

Implementing the Server

Here's the implementation of our Express.js server:


// index.js
const express = require('express');
const cors = require('cors');
const { v4: uuidv4 } = require('uuid');

// Initialize the app
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(cors());
app.use(express.json());

// In-memory data store
let tasks = [];

// Helper function to find task by ID
function findTaskById(id) {
  return tasks.find(task => task.id === id);
}

// GET all tasks
app.get('/api/tasks', (req, res) => {
  res.json(tasks);
});

// GET a single task by ID
app.get('/api/tasks/:id', (req, res) => {
  const task = findTaskById(req.params.id);
  
  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }
  
  res.json(task);
});

// POST a new task
app.post('/api/tasks', (req, res) => {
  const { title, description } = req.body;
  
  // Validation
  if (!title) {
    return res.status(400).json({ error: 'Title is required' });
  }
  
  const newTask = {
    id: uuidv4(),
    title,
    description: description || '',
    status: 'pending',
    created_at: new Date().toISOString()
  };
  
  tasks.push(newTask);
  res.status(201).json(newTask);
});

// PUT (update) a task
app.put('/api/tasks/:id', (req, res) => {
  const { title, description, status } = req.body;
  const task = findTaskById(req.params.id);
  
  if (!task) {
    return res.status(404).json({ error: 'Task not found' });
  }
  
  // Validation
  if (status && !['pending', 'in-progress', 'completed'].includes(status)) {
    return res.status(400).json({ error: 'Invalid status value' });
  }
  
  // Update task
  if (title) task.title = title;
  if (description !== undefined) task.description = description;
  if (status) task.status = status;
  
  res.json(task);
});

// DELETE a task
app.delete('/api/tasks/:id', (req, res) => {
  const taskIndex = tasks.findIndex(task => task.id === req.params.id);
  
  if (taskIndex === -1) {
    return res.status(404).json({ error: 'Task not found' });
  }
  
  tasks.splice(taskIndex, 1);
  res.json({ message: 'Task deleted successfully' });
});

// Start the server
app.listen(PORT, () => {
  console.log(`Node.js server running on port ${PORT}`);
});
        

Running the Node.js Server


# Start the server
node index.js

# You should see: "Node.js server running on port 3000"
        

Python Implementation with Flask

Setting Up the Project


# Create project directory
mkdir -p weekend-project/python-server
cd weekend-project/python-server

# Create a virtual environment
python -m venv venv

# Activate the virtual environment
# On Windows:
# venv\Scripts\activate
# On macOS/Linux:
source venv/bin/activate

# Install dependencies
pip install flask uuid flask-cors

# Create requirements.txt
pip freeze > requirements.txt

# Create the main file
touch app.py
        

Implementing the Server

Here's the implementation of our Flask server:


# app.py
from flask import Flask, request, jsonify
from flask_cors import CORS
import uuid
from datetime import datetime

# Initialize the app
app = Flask(__name__)
CORS(app)

# In-memory data store
tasks = []

# Helper function to find task by ID
def find_task_by_id(task_id):
    for task in tasks:
        if task['id'] == task_id:
            return task
    return None

# GET all tasks
@app.route('/api/tasks', methods=['GET'])
def get_all_tasks():
    return jsonify(tasks)

# GET a single task by ID
@app.route('/api/tasks/', methods=['GET'])
def get_task(task_id):
    task = find_task_by_id(task_id)
    
    if not task:
        return jsonify({'error': 'Task not found'}), 404
    
    return jsonify(task)

# POST a new task
@app.route('/api/tasks', methods=['POST'])
def create_task():
    data = request.get_json()
    
    # Validation
    if not data or 'title' not in data:
        return jsonify({'error': 'Title is required'}), 400
    
    new_task = {
        'id': str(uuid.uuid4()),
        'title': data['title'],
        'description': data.get('description', ''),
        'status': 'pending',
        'created_at': datetime.now().isoformat()
    }
    
    tasks.append(new_task)
    return jsonify(new_task), 201

# PUT (update) a task
@app.route('/api/tasks/', methods=['PUT'])
def update_task(task_id):
    task = find_task_by_id(task_id)
    
    if not task:
        return jsonify({'error': 'Task not found'}), 404
    
    data = request.get_json()
    
    # Validation
    if 'status' in data and data['status'] not in ['pending', 'in-progress', 'completed']:
        return jsonify({'error': 'Invalid status value'}), 400
    
    # Update task
    if 'title' in data:
        task['title'] = data['title']
    if 'description' in data:
        task['description'] = data['description']
    if 'status' in data:
        task['status'] = data['status']
    
    return jsonify(task)

# DELETE a task
@app.route('/api/tasks/', methods=['DELETE'])
def delete_task(task_id):
    global tasks
    task = find_task_by_id(task_id)
    
    if not task:
        return jsonify({'error': 'Task not found'}), 404
    
    tasks = [t for t in tasks if t['id'] != task_id]
    return jsonify({'message': 'Task deleted successfully'})

if __name__ == '__main__':
    app.run(debug=True, port=5000)
        

Running the Python Server


# Start the server
python app.py

# You should see: "Running on http://127.0.0.1:5000/"
        

PHP Implementation

Setting Up the Project


# Create project directory
mkdir -p weekend-project/php-server
cd weekend-project/php-server

# Create necessary files
touch index.php
touch router.php
mkdir data
touch data/tasks.json
        

Implementing the Server

Here's the implementation of our PHP server:


<?php
// index.php
// Set headers for CORS and JSON response
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Content-Type: application/json');

// Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    exit(0);
}

// Include router
require_once 'router.php';

// Parse the URL and route the request
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$uri = explode('/', $uri);

// API routes should start with /api/tasks
if ($uri[1] !== 'api' || $uri[2] !== 'tasks') {
    header("HTTP/1.1 404 Not Found");
    echo json_encode(['error' => 'Not Found']);
    exit();
}

// Get the task ID if it exists
$taskId = null;
if (isset($uri[3])) {
    $taskId = $uri[3];
}

// Route the request based on the HTTP method
$method = $_SERVER['REQUEST_METHOD'];

switch ($method) {
    case 'GET':
        if ($taskId) {
            getTask($taskId);
        } else {
            getAllTasks();
        }
        break;
    case 'POST':
        createTask();
        break;
    case 'PUT':
        if ($taskId) {
            updateTask($taskId);
        } else {
            header("HTTP/1.1 400 Bad Request");
            echo json_encode(['error' => 'Task ID is required']);
        }
        break;
    case 'DELETE':
        if ($taskId) {
            deleteTask($taskId);
        } else {
            header("HTTP/1.1 400 Bad Request");
            echo json_encode(['error' => 'Task ID is required']);
        }
        break;
    default:
        header("HTTP/1.1 405 Method Not Allowed");
        echo json_encode(['error' => 'Method not allowed']);
        break;
}

<?php
// router.php

// Load tasks from JSON file
function getTasks() {
    if (file_exists('data/tasks.json')) {
        $json = file_get_contents('data/tasks.json');
        return json_decode($json, true) ?: [];
    }
    return [];
}

// Save tasks to JSON file
function saveTasks($tasks) {
    file_put_contents('data/tasks.json', json_encode($tasks, JSON_PRETTY_PRINT));
}

// Find task by ID
function findTaskById($id) {
    $tasks = getTasks();
    foreach ($tasks as $task) {
        if ($task['id'] === $id) {
            return $task;
        }
    }
    return null;
}

// GET all tasks
function getAllTasks() {
    echo json_encode(getTasks());
}

// GET a single task by ID
function getTask($id) {
    $task = findTaskById($id);
    
    if (!$task) {
        header("HTTP/1.1 404 Not Found");
        echo json_encode(['error' => 'Task not found']);
        return;
    }
    
    echo json_encode($task);
}

// POST a new task
function createTask() {
    $data = json_decode(file_get_contents('php://input'), true);
    
    // Validation
    if (!isset($data['title']) || empty($data['title'])) {
        header("HTTP/1.1 400 Bad Request");
        echo json_encode(['error' => 'Title is required']);
        return;
    }
    
    $tasks = getTasks();
    
    // Generate a unique ID
    $id = uniqid();
    
    $newTask = [
        'id' => $id,
        'title' => $data['title'],
        'description' => $data['description'] ?? '',
        'status' => 'pending',
        'created_at' => date('c') // ISO 8601 date
    ];
    
    $tasks[] = $newTask;
    saveTasks($tasks);
    
    header("HTTP/1.1 201 Created");
    echo json_encode($newTask);
}

// PUT (update) a task
function updateTask($id) {
    $task = findTaskById($id);
    
    if (!$task) {
        header("HTTP/1.1 404 Not Found");
        echo json_encode(['error' => 'Task not found']);
        return;
    }
    
    $data = json_decode(file_get_contents('php://input'), true);
    
    // Validation
    if (isset($data['status']) && !in_array($data['status'], ['pending', 'in-progress', 'completed'])) {
        header("HTTP/1.1 400 Bad Request");
        echo json_encode(['error' => 'Invalid status value']);
        return;
    }
    
    // Update task
    if (isset($data['title'])) {
        $task['title'] = $data['title'];
    }
    if (array_key_exists('description', $data)) {
        $task['description'] = $data['description'];
    }
    if (isset($data['status'])) {
        $task['status'] = $data['status'];
    }
    
    // Update the task in the array
    $tasks = getTasks();
    foreach ($tasks as $key => $t) {
        if ($t['id'] === $id) {
            $tasks[$key] = $task;
            break;
        }
    }
    
    saveTasks($tasks);
    echo json_encode($task);
}

// DELETE a task
function deleteTask($id) {
    $tasks = getTasks();
    $found = false;
    
    foreach ($tasks as $key => $task) {
        if ($task['id'] === $id) {
            unset($tasks[$key]);
            $found = true;
            break;
        }
    }
    
    if (!$found) {
        header("HTTP/1.1 404 Not Found");
        echo json_encode(['error' => 'Task not found']);
        return;
    }
    
    // Reindex array
    $tasks = array_values($tasks);
    saveTasks($tasks);
    
    echo json_encode(['message' => 'Task deleted successfully']);
}

Running the PHP Server


# Start the built-in PHP server
php -S localhost:8000

# You should see: "PHP 8.x.x Development Server started at [date and time]"
        

Creating Empty tasks.json File


# For the PHP implementation, create an empty JSON array
echo "[]" > data/tasks.json
        

Step 4: Review and Extend

Now that we've implemented our API in all three technologies, let's review what we've learned and look at how we can extend our solutions.

Testing the APIs

To test the APIs, we'll use Postman or any HTTP client. Here are the test cases to try:

  1. GET all tasks:
    • Node.js: GET http://localhost:3000/api/tasks
    • Python: GET http://localhost:5000/api/tasks
    • PHP: GET http://localhost:8000/api/tasks
  2. Create a task:
    • Node.js: POST http://localhost:3000/api/tasks
    • Python: POST http://localhost:5000/api/tasks
    • PHP: POST http://localhost:8000/api/tasks
    • Request body: { "title": "Learn Node.js", "description": "Study Express.js framework" }
  3. Get a single task:
    • Node.js: GET http://localhost:3000/api/tasks/{id}
    • Python: GET http://localhost:5000/api/tasks/{id}
    • PHP: GET http://localhost:8000/api/tasks/{id}
  4. Update a task:
    • Node.js: PUT http://localhost:3000/api/tasks/{id}
    • Python: PUT http://localhost:5000/api/tasks/{id}
    • PHP: PUT http://localhost:8000/api/tasks/{id}
    • Request body: { "status": "in-progress" }
  5. Delete a task:
    • Node.js: DELETE http://localhost:3000/api/tasks/{id}
    • Python: DELETE http://localhost:5000/api/tasks/{id}
    • PHP: DELETE http://localhost:8000/api/tasks/{id}

Comparing the Implementations

Feature Node.js (Express) Python (Flask) PHP
Routing Express's app.METHOD(path, handler) Flask's @app.route decorator Manual routing with switch/case
Request Parsing req.body (with express.json() middleware) request.get_json() file_get_contents('php://input')
Route Parameters req.params.id Function parameter from route Manual URL parsing
Status Codes res.status(code).json(data) return jsonify(data), status_code header("HTTP/1.1 XXX Status")
Data Storage In-memory array (lost on restart) In-memory list (lost on restart) JSON file (persists between restarts)
Error Handling return res.status(404).json({ error: 'msg' }) return jsonify({'error': 'msg'}), 404 header() + json_encode()
Code Organization Single file (can be modularized) Single file (can be modularized) Split into index and router

Language-Specific Observations

Node.js Observations

Python (Flask) Observations

PHP Observations

Extensions and Improvements

Here are some ways you can extend these basic implementations:

Common Extensions

  1. Database Integration: Replace in-memory storage with a database
    • Node.js: MongoDB with Mongoose or SQL with Sequelize
    • Python: SQLAlchemy or PyMongo
    • PHP: PDO for SQL databases
  2. Authentication: Add user authentication
    • Node.js: Passport.js or JWT
    • Python: Flask-Login or PyJWT
    • PHP: JWT or session-based auth
  3. Input Validation: Add more robust validation
    • Node.js: Joi or express-validator
    • Python: Marshmallow or Pydantic
    • PHP: Respect\Validation or Laravel Validator
  4. Logging: Add request logging
    • Node.js: Morgan or Winston
    • Python: Flask's built-in logger or Loguru
    • PHP: Monolog
  5. Testing: Add automated tests
    • Node.js: Jest or Mocha
    • Python: pytest
    • PHP: PHPUnit

Node.js-Specific Extensions

Python-Specific Extensions

PHP-Specific Extensions

Applying Polya's Problem-Solving Process

Throughout this project, we've applied George Polya's four-step problem-solving process. Let's reflect on how each step contributed to our solution:

flowchart TD A[Problem Solving Process] --> B[1. Understand
the Problem] A --> C[2. Devise
a Plan] A --> D[3. Execute
the Plan] A --> E[4. Review/Extend] B --> B1[We defined our API structure,
endpoints, and data models] B --> B2[We clarified the requirements
for each technology] C --> C1[We designed the architecture
for each implementation] C --> C2[We identified common components
and technology-specific approaches] D --> D1[We implemented the
Node.js server with Express] D --> D2[We implemented the
Python server with Flask] D --> D3[We implemented the
PHP server with a custom router] E --> E1[We tested each API
implementation] E --> E2[We compared the approaches
and identified pros/cons] E --> E3[We proposed extensions
and improvements] style A fill:#f5f5f5,stroke:#333,stroke-width:2px style B fill:#dbeafe,stroke:#333,stroke-width:2px style C fill:#c6f6d5,stroke:#333,stroke-width:2px style D fill:#fef3c7,stroke:#333,stroke-width:2px style E fill:#fecdd3,stroke:#333,stroke-width:2px
  1. Understand the Problem:
    • We clearly defined what we needed to build (a task management API)
    • We specified the data model and API endpoints
    • We identified the technologies we would use and their characteristics
    • We articulated key questions to address in our implementation
  2. Devise a Plan:
    • We created a high-level plan for implementing each server
    • We broke down the common components across all implementations
    • We outlined technology-specific approaches
    • We organized the project structure
  3. Execute the Plan:
    • We set up the development environments for each technology
    • We implemented the APIs following our planned approach
    • We ensured each implementation met the requirements
    • We kept the code organized and documented
  4. Review/Extend:
    • We tested each implementation to verify functionality
    • We compared the different approaches and identified strengths and weaknesses
    • We suggested improvements and extensions
    • We reflected on what we learned from each technology

Additional Challenge: Building a Simple Frontend

As an additional challenge, consider building a simple HTML/CSS/JavaScript frontend that can interact with any of your API servers. This will give you a complete full-stack experience.

Here's a simple HTML file you can use as a starting point:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Task Manager</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        
        h1 {
            color: #333;
        }
        
        .task-form {
            margin-bottom: 20px;
            padding: 15px;
            background-color: #f5f5f5;
            border-radius: 5px;
        }
        
        .form-group {
            margin-bottom: 15px;
        }
        
        label {
            display: block;
            margin-bottom: 5px;
        }
        
        input, textarea {
            width: 100%;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        
        button {
            background-color: #4CAF50;
            color: white;
            border: none;
            padding: 10px 15px;
            border-radius: 4px;
            cursor: pointer;
        }
        
        button:hover {
            background-color: #45a049;
        }
        
        .task-list {
            list-style: none;
            padding: 0;
        }
        
        .task-item {
            background-color: #fff;
            border: 1px solid #ddd;
            border-radius: 4px;
            margin-bottom: 10px;
            padding: 15px;
        }
        
        .task-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 10px;
        }
        
        .task-title {
            font-weight: bold;
            font-size: 18px;
        }
        
        .task-status {
            padding: 5px 10px;
            border-radius: 15px;
            font-size: 12px;
        }
        
        .status-pending {
            background-color: #f8d7da;
            color: #721c24;
        }
        
        .status-in-progress {
            background-color: #fff3cd;
            color: #856404;
        }
        
        .status-completed {
            background-color: #d4edda;
            color: #155724;
        }
        
        .task-actions {
            margin-top: 10px;
        }
        
        .btn-delete {
            background-color: #dc3545;
        }
        
        .btn-delete:hover {
            background-color: #c82333;
        }
    </style>
</head>
<body>
    <h1>Task Manager</h1>
    
    <div class="task-form">
        <h2>Add New Task</h2>
        <form id="task-form">
            <div class="form-group">
                <label for="title">Title:</label>
                <input type="text" id="title" required>
            </div>
            
            <div class="form-group">
                <label for="description">Description:</label>
                <textarea id="description" rows="3"></textarea>
            </div>
            
            <button type="submit">Add Task</button>
        </form>
    </div>
    
    <h2>Tasks</h2>
    <ul id="task-list" class="task-list">
        <!-- Tasks will be inserted here -->
    </ul>
    
    <script>
        // API URL - Change this to point to your server
        const API_URL = 'http://localhost:3000/api/tasks'; // Change port as needed

        // DOM Elements
        const taskForm = document.getElementById('task-form');
        const taskList = document.getElementById('task-list');
        const titleInput = document.getElementById('title');
        const descriptionInput = document.getElementById('description');

        // Event Listeners
        document.addEventListener('DOMContentLoaded', fetchTasks);
        taskForm.addEventListener('submit', addTask);

        // Fetch all tasks
        async function fetchTasks() {
            try {
                const response = await fetch(API_URL);
                const tasks = await response.json();
                renderTasks(tasks);
            } catch (error) {
                console.error('Error fetching tasks:', error);
            }
        }

        // Add a new task
        async function addTask(e) {
            e.preventDefault();
            
            const task = {
                title: titleInput.value,
                description: descriptionInput.value
            };
            
            try {
                const response = await fetch(API_URL, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify(task)
                });
                
                const newTask = await response.json();
                renderTask(newTask);
                
                // Clear form
                taskForm.reset();
            } catch (error) {
                console.error('Error adding task:', error);
            }
        }

        // Update task status
        async function updateTaskStatus(id, status) {
            try {
                const response = await fetch(`${API_URL}/${id}`, {
                    method: 'PUT',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ status })
                });
                
                await response.json();
                fetchTasks(); // Refresh the list
            } catch (error) {
                console.error('Error updating task:', error);
            }
        }

        // Delete a task
        async function deleteTask(id) {
            try {
                await fetch(`${API_URL}/${id}`, {
                    method: 'DELETE'
                });
                
                // Remove from DOM
                const taskElement = document.getElementById(`task-${id}`);
                if (taskElement) {
                    taskElement.remove();
                }
            } catch (error) {
                console.error('Error deleting task:', error);
            }
        }

        // Render all tasks
        function renderTasks(tasks) {
            taskList.innerHTML = '';
            tasks.forEach(task => renderTask(task));
        }

        // Render a single task
        function renderTask(task) {
            const li = document.createElement('li');
            li.id = `task-${task.id}`;
            li.className = 'task-item';
            
            const statusClass = `status-${task.status.replace(' ', '-')}`;
            
            li.innerHTML = `
                <div class="task-header">
                    <span class="task-title">${task.title}</span>
                    <span class="task-status ${statusClass}">${task.status}</span>
                </div>
                <div class="task-description">${task.description || ''}</div>
                <div class="task-actions">
                    ${task.status !== 'in-progress' ? 
                        `<button onclick="updateTaskStatus('${task.id}', 'in-progress')">Mark In Progress</button>` : ''}
                    ${task.status !== 'completed' ? 
                        `<button onclick="updateTaskStatus('${task.id}', 'completed')">Mark Completed</button>` : ''}
                    ${task.status !== 'pending' ? 
                        `<button onclick="updateTaskStatus('${task.id}', 'pending')">Mark Pending</button>` : ''}
                    <button class="btn-delete" onclick="deleteTask('${task.id}')">Delete</button>
                </div>
            `;
            
            taskList.appendChild(li);
        }
    </script>
</body>
</html>
        

Save this as index.html in a separate directory and open it in your browser. Make sure to adjust the API_URL to point to whichever server you want to use.

Final Thoughts and Reflections

Key Learning Points

Final Challenge

Based on what you've learned, consider which technology you prefer for backend development and why. Create a more complex application in that technology to deepen your skills.

Next Steps

Resources and References

Node.js and Express

Python and Flask

PHP

RESTful APIs

Problem Solving and Design