← Back to list

Building a RESTful Todo API in C: A Step-by-Step Guide for Beginners

In this comprehensive tutorial, we’ll walk through the process of building a RESTful API in C from scratch. By the end, you’ll have a fully…

trish · 2025-04-20 19:01 · 23 claps · 34.9 min read paywalled
#c #rest-api #sql #programming #tutorial
Open on Medium ↗
Wiki topics: 💻 · Programming

Building a RESTful Todo API in C: A Step-by-Step Guide for Beginners

In this comprehensive tutorial, we’ll walk through the process of building a RESTful API in C from scratch. By the end, you’ll have a fully functional Todo API that can create, read, update, and delete tasks using standard HTTP methods.

Introduction

Building a RESTful API in C is an excellent way to understand how web services work at a low level. While C isn’t typically the first choice for web development, it’s powerful for understanding the underlying mechanics, and it’s still used in many production systems where performance is critical.

In this tutorial, we’ll build a Todo API that:

  • Uses SQLite for data storage
  • Implements a RESTful interface with libmicrohttpd
  • Processes JSON with jansson
  • Handles HTTP requests

This project is perfect for beginners who want to understand how REST APIs work from the ground up.

What is a REST API?

REST (Representational State Transfer) is an architectural style for designing networked applications. RESTful APIs use HTTP requests to perform CRUD (Create, Read, Update, Delete) operations on resources.

Key principles of REST include:

  1. Statelessness: Each request from a client to server must contain all the information needed to understand and process the request.
  2. Client-Server Architecture: The client and server are separate entities that communicate over HTTP.
  3. Uniform Interface: Resources are identified by URIs (Uniform Resource Identifiers), and standard HTTP methods (GET, POST, PUT, DELETE) are used to manipulate them.
  4. Resource-Based: Everything is treated as a resource that can be created, read, updated, or deleted.

For example, in our Todo API:

  • GET /todos retrieves a list of all todos
  • GET /todos/1 retrieves the todo with ID 1
  • POST /todos creates a new todo
  • PUT /todos/1 updates the todo with ID 1
  • DELETE /todos/1 deletes the todo with ID 1

Why Build an API in C?

While modern web development often uses high-level languages and frameworks, building an API in C offers several advantages:

  1. Performance: C provides exceptional performance with minimal overhead.
  2. Control: You have direct control over memory management and system resources.
  3. Learning: Understanding how things work at a lower level strengthens your knowledge of web technologies.
  4. Portability: C code can run on virtually any platform with minimal changes.
  5. Integration: C makes it easy to interface with system libraries and low-level components.

Key Concepts

Before diving into the code, let’s understand some key concepts we’ll be working with:

  1. HTTP (Hypertext Transfer Protocol): The foundation of data communication on the web. Our API will respond to HTTP requests using the libmicrohttpd library.
  2. JSON (JavaScript Object Notation): A lightweight data interchange format. We’ll use the jansson library to parse and generate JSON.
  3. SQLite: A self-contained, serverless database engine. We’ll use it to store and retrieve todo items.
  4. CRUD Operations: The four basic functions of persistent storage:
  • Create: Adding new resources
  • Read: Retrieving existing resources
  • Update: Modifying existing resources
  • Delete: Removing resources

5. HTTP Methods:

  • GET: Retrieve data
  • POST: Create new resources
  • PUT: Update existing resources
  • DELETE: Remove resources

6. Status Codes: HTTP response status codes indicate whether a request was successful or not:

  • 200 OK: The request was successful
  • 201 Created: The request was successful and a resource was created
  • 400 Bad Request: The request is malformed
  • 404 Not Found: The requested resource doesn’t exist
  • 500 Internal Server Error: Something went wrong on the server

Now that we understand the key concepts, let’s dive into building our API!

Project Overview

Our Todo API will support the following operations:

  1. List all todos: GET /todos
  2. Get a specific todo: GET /todos/:id
  3. Create a new todo: POST /todos
  4. Update a todo: PUT /todos/:id
  5. Delete a todo: DELETE /todos/:id

Each todo item will have the following properties:

  • id: Unique identifier (integer)
  • title: Brief title of the task (string)
  • description: Detailed description (string)
  • completed: Boolean status (0 for incomplete, 1 for complete)
  • created_at: Timestamp of creation
  • updated_at: Timestamp of last update

Setting Up the Development Environment

Before we start coding, we need to install the necessary dependencies. We’ll need several libraries for our project:

  1. libmicrohttpd: A small C library that makes it easy to run an HTTP server as part of another application. We’ll use this to handle HTTP requests and responses.
  2. libsqlite3: The C library for SQLite, a self-contained, serverless, zero-configuration, transactional SQL database engine. We’ll use this for data persistence.
  3. libjansson: A C library for encoding, decoding, and manipulating JSON data. We’ll use this to parse incoming JSON requests and format JSON responses.
  4. libcurl: A client-side URL transfer library supporting various protocols. We’ll use this primarily for testing our API.
  5. CMake: A cross-platform build system generator. We’ll use this to manage the building of our project.

Let’s install these dependencies on Debian/Ubuntu:

sudo apt-get update
sudo apt-get install build-essential cmake libcurl4-openssl-dev libsqlite3-dev libmicrohttpd-dev libjansson-dev

Understanding the Libraries

Let’s explore each library we’ll be using in more detail:

libmicrohttpd

libmicrohttpd is a small, lightweight HTTP server library that allows us to embed a web server into our application. It handles the low-level details of HTTP communication, like parsing headers and managing connections.

Key features we’ll use:

  • Starting and stopping the HTTP server
  • Handling different HTTP methods (GET, POST, PUT, DELETE)
  • Processing request headers and bodies
  • Sending responses with appropriate status codes

SQLite

SQLite is a C library that provides a lightweight, disk-based database. Unlike other database systems, SQLite doesn’t require a separate server process and allows accessing the database directly from our application.

Key features we’ll use:

  • Creating and connecting to a database
  • Defining tables to store our todo items
  • Executing SQL queries to perform CRUD operations
  • Using prepared statements to prevent SQL injection

Jansson

Jansson is a C library for encoding, decoding, and manipulating JSON data. It provides an easy-to-use API for working with JSON objects.

Key features we’ll use:

  • Parsing JSON strings into C data structures
  • Creating JSON objects and arrays
  • Accessing JSON object properties
  • Converting JSON data to strings for HTTP responses

libcurl

libcurl is a powerful client-side URL transfer library. While we’re primarily building a server, we’ll use libcurl to help with testing our API by making HTTP requests.

Key features we’ll use:

  • Making HTTP requests to test our server
  • Setting request headers and body data
  • Processing response data

Now that we have our environment set up, let’s move on to structuring our project.

Project Structure

A well-organized project structure is crucial for maintainability and understanding. We’ll use a modular approach to organize our code, separating different concerns into distinct files and directories.

Let’s create the following structure:

rest-api/
├── CMakeLists.txt              # Main CMake configuration
├── manage.sh                   # Management script
├── src/                        # Source code
│   ├── CMakeLists.txt          # Source CMake configuration
│   ├── main.c                  # Entry point
│   ├── core/                   # Core functionality
│   │   ├── todo.h              # Todo structure definition
│   │   └── todo.c              # Todo operations
│   ├── db/                     # Database operations
│   │   ├── database.h          # Database interface
│   │   └── database.c          # SQLite implementation
│   └── http/                   # HTTP handling
│       ├── server.h            # Server interface
│       ├── server.c            # Server implementation
│       ├── handlers.h          # Request handlers interface
│       └── handlers.c          # Request handlers implementation
├── tests/                      # Unit tests
│   ├── CMakeLists.txt          # Test CMake configuration
│   └── test_todo.c             # Todo unit tests
└── scripts/                    # Helper scripts
    └── test_api.sh             # API test script

Explaining the Structure

1. Root Directory

  • CMakeLists.txt: The main CMake build configuration file that sets up the project, finds dependencies, and includes subdirectories.
  • manage.sh: A shell script that provides commands for building, running, and testing the application.

2. Source Directory (src/)

This directory contains all the source code for our API, divided into logical modules:

  • main.c: The entry point of our application that initializes components and starts the server.
  • Core Module (core/): Contains the core data structures and business logic.
  • todo.h: Defines the todo_t structure and function prototypes for todo operations.
  • todo.c: Implements the todo operations (create, read, update, delete).
  • Database Module (db/): Handles database operations.
  • database.h: Provides a simple interface for database operations.
  • database.c: Implements the database interface using SQLite.
  • HTTP Module (http/): Manages HTTP communication.
  • server.h: Defines the functions for starting and stopping the HTTP server.
  • server.c: Implements the HTTP server using libmicrohttpd.
  • handlers.h: Declares handler functions for different HTTP endpoints.
  • handlers.c: Implements the request handlers for each API endpoint.

3. Tests Directory (tests/)

  • test_todo.c: Contains unit tests for the todo operations.
  • CMakeLists.txt: Configuration for building and running tests.

4. Scripts Directory (scripts/)

  • test_api.sh: A shell script for testing the API endpoints.

How the Components Work Together

  1. Initialization Flow:
  • main.c initializes the database using functions from database.c
  • It then starts the HTTP server defined in server.c
  • The server runs until the program is terminated

2. Request Handling Flow:

  • server.c receives HTTP requests
  • It parses the URL and HTTP method
  • Based on the URL and method, it calls the appropriate handler function in handlers.c
  • The handler uses functions from todo.c to perform database operations
  • The handler then formats the response and sends it back

3. Data Flow:

  • HTTP requests come in through server.c
  • Request data is parsed and passed to handlers in handlers.c
  • Handlers call functions in todo.c to manipulate the todo items
  • todo.c uses functions from database.c to interact with the SQLite database
  • Results flow back up through the same chain

This modular design makes our code more maintainable, testable, and easier to understand. Each component has a single responsibility and clear interfaces with other components.

Let’s start by creating these directories:

mkdir -p rest-api/src/core rest-api/src/db rest-api/src/http rest-api/tests rest-api/scripts
cd rest-api

Now that we have our project structure in place, let’s move on to implementing the core components.

Core Components

Now that we have our project structure in place, let’s implement the core components of our API. We’ll start with the todo data structure, then move on to database management, HTTP request handling, and JSON processing.

Todo Data Structure

The todo data structure is the central component of our API. It represents a task that a user wants to track. Let’s define the structure and the operations we’ll need to perform on it.

Todo Header File (src/core/todo.h)

First, let’s create the header file for our todo structure:

/**
 * @file todo.h
 * @brief Defines the todo data structure and operations
 */

#ifndef TODO_H
#define TODO_H
#include <time.h>
/**
 * @struct todo_t
 * @brief Represents a todo item
 */
typedef struct {
    int id;                 /**< Unique identifier */
    char title[100];        /**< Brief title of the task */
    char description[1000]; /**< Detailed description */
    int completed;          /**< Completion status (0=incomplete, 1=complete) */
    time_t created_at;      /**< Creation timestamp */
    time_t updated_at;      /**< Last update timestamp */
} todo_t;
/**
 * @brief Creates a new todo item
 * @param title The title of the todo
 * @param description The description of the todo
 * @return 0 on success, -1 on failure
 */
int todo_create(const char* title, const char* description);
/**
 * @brief Retrieves a todo item by ID
 * @param id The ID of the todo to retrieve
 * @param todo Pointer to store the retrieved todo
 * @return 0 on success, -1 on failure
 */
int todo_get(int id, todo_t* todo);
/**
 * @brief Updates an existing todo item
 * @param id The ID of the todo to update
 * @param title The new title
 * @param description The new description
 * @param completed The new completion status
 * @return 0 on success, -1 on failure
 */
int todo_update(int id, const char* title, const char* description, int completed);
/**
 * @brief Deletes a todo item
 * @param id The ID of the todo to delete
 * @return 0 on success, -1 on failure
 */
int todo_delete(int id);
/**
 * @brief Lists all todo items
 * @param todos Pointer to store the array of todos (must be freed by caller)
 * @param count Pointer to store the number of todos
 * @return 0 on success, -1 on failure
 */
int todo_list(todo_t** todos, int* count);
/**
 * @brief Frees memory allocated for a list of todos
 * @param todos Pointer to the todo array to free
 */
void todo_free_list(todo_t* todos);
#endif /* TODO_H */

Let’s break down what we’ve defined:

  1. The todo_t structure: Contains all the information about a todo item, including its ID, title, description, completion status, and timestamps.
  2. Function prototypes: We’ve declared six functions to perform operations on todo items:
  • todo_create: Creates a new todo item
  • todo_get: Retrieves a todo item by ID
  • todo_update: Updates an existing todo item
  • todo_delete: Deletes a todo item
  • todo_list: Lists all todo items
  • todo_free_list: Frees memory allocated for a list of todos

Todo Implementation File (src/core/todo.c)

Now, let’s implement these functions in the todo.c file:

/**
 * @file todo.c
 * @brief Implements the todo operations
 */

#include "todo.h"
#include "../db/database.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/**
 * @brief Creates a new todo item
 * @param title The title of the todo
 * @param description The description of the todo
 * @return 0 on success, -1 on failure
 */
int todo_create(const char* title, const char* description) {
    time_t now = time(NULL);
    // Prepare the SQL statement with proper escaping for strings
    // Note: In a production environment, you would use prepared statements
    // to prevent SQL injection, but for simplicity, we'll use string formatting
    char sql[2048];
    snprintf(sql, sizeof(sql), 
             "INSERT INTO todos (title, description, completed, created_at, updated_at) "
             "VALUES ('%s', '%s', 0, %ld, %ld)",
             title, description, now, now);
    return db_execute(sql);
}
/**
 * @brief Retrieves a todo item by ID
 * @param id The ID of the todo to retrieve
 * @param todo Pointer to store the retrieved todo
 * @return 0 on success, -1 on failure
 */
int todo_get(int id, todo_t* todo) {
    char sql[256];
    snprintf(sql, sizeof(sql), "SELECT * FROM todos WHERE id = %d", id);
    // Define a callback to populate the todo struct
    int callback(void* data, int argc, char** argv, char** azColName) {
        todo_t* t = (todo_t*)data;
        t->id = atoi(argv[0]);
        strncpy(t->title, argv[1], sizeof(t->title) - 1);
        t->title[sizeof(t->title) - 1] = '\0'; // Ensure null termination
        strncpy(t->description, argv[2], sizeof(t->description) - 1);
        t->description[sizeof(t->description) - 1] = '\0'; // Ensure null termination
        t->completed = atoi(argv[3]);
        t->created_at = atol(argv[4]);
        t->updated_at = atol(argv[5]);
        return 0;
    }
    // Zero out the todo struct before populating it
    memset(todo, 0, sizeof(todo_t));
    // Execute the query with the callback
    int result = db_query_callback(sql, callback, todo);
    // If no rows were returned (id field is still 0), return an error
    if (todo->id == 0) {
        return -1;
    }
    return result;
}
/**
 * @brief Updates an existing todo item
 * @param id The ID of the todo to update
 * @param title The new title
 * @param description The new description
 * @param completed The new completion status
 * @return 0 on success, -1 on failure
 */
int todo_update(int id, const char* title, const char* description, int completed) {
    time_t now = time(NULL);
    char sql[2048];
    snprintf(sql, sizeof(sql), 
             "UPDATE todos SET title = '%s', description = '%s', "
             "completed = %d, updated_at = %ld WHERE id = %d",
             title, description, completed, now, id);
    return db_execute(sql);
}
/**
 * @brief Deletes a todo item
 * @param id The ID of the todo to delete
 * @return 0 on success, -1 on failure
 */
int todo_delete(int id) {
    char sql[256];
    snprintf(sql, sizeof(sql), "DELETE FROM todos WHERE id = %d", id);
    return db_execute(sql);
}
/**
 * @brief Structure to hold todo list data during callback
 */
typedef struct {
    todo_t* todos;  /**< Array of todos */
    int count;      /**< Number of todos */
    int capacity;   /**< Capacity of the todos array */
} todo_list_data_t;
/**
 * @brief Lists all todo items
 * @param todos Pointer to store the array of todos (must be freed by caller)
 * @param count Pointer to store the number of todos
 * @return 0 on success, -1 on failure
 */
int todo_list(todo_t** todos, int* count) {
    const char* sql = "SELECT * FROM todos ORDER BY id";
    // Initialize the list data
    todo_list_data_t data = {NULL, 0, 0};
    // Define a callback to add todos to the list
    int callback(void* user_data, int argc, char** argv, char** azColName) {
        todo_list_data_t* data = (todo_list_data_t*)user_data;
        // Grow the array if needed
        if (data->count >= data->capacity) {
            data->capacity = data->capacity == 0 ? 10 : data->capacity * 2;
            data->todos = realloc(data->todos, data->capacity * sizeof(todo_t));
            if (!data->todos) {
                return 1; // Out of memory
            }
        }
        // Add the new todo to the array
        todo_t* todo = &data->todos[data->count++];
        todo->id = atoi(argv[0]);
        strncpy(todo->title, argv[1], sizeof(todo->title) - 1);
        todo->title[sizeof(todo->title) - 1] = '\0'; // Ensure null termination
        strncpy(todo->description, argv[2], sizeof(todo->description) - 1);
        todo->description[sizeof(todo->description) - 1] = '\0'; // Ensure null termination
        todo->completed = atoi(argv[3]);
        todo->created_at = atol(argv[4]);
        todo->updated_at = atol(argv[5]);
        return 0;
    }
    // Execute the query with the callback
    int result = db_query_callback(sql, callback, &data);
    // Set the output parameters
    *todos = data.todos;
    *count = data.count;
    return result;
}
/**
 * @brief Frees memory allocated for a list of todos
 * @param todos Pointer to the todo array to free
 */
void todo_free_list(todo_t* todos) {
    free(todos);
}

Let’s analyze the implementation of each function:

  1. **todo_create**:
  • Takes a title and description as input.
  • Gets the current time to use for the creation and update timestamps.
  • Constructs an SQL INSERT statement to add the todo to the database.
  • Uses the database execution function to run the query.

**2. todo_get**:

  • Takes an ID and a pointer to a todo structure.
  • Constructs an SQL SELECT statement to fetch the todo with the given ID.
  • Defines a callback function that will be called with the query results.
  • The callback populates the todo structure with data from the query.
  • Returns an error if no todo with the given ID is found.

**3. todo_update**:

  • Takes an ID, title, description, and completion status.
  • Gets the current time for the update timestamp.
  • Constructs an SQL UPDATE statement to modify the todo in the database.
  • Uses the database execution function to run the query.

**4. todo_delete**:

  • Takes an ID as input.
  • Constructs an SQL DELETE statement to remove the todo from the database.
  • Uses the database execution function to run the query.

**4. todo_list**:

  • Takes pointers to store the list of todos and the count.
  • Constructs an SQL SELECT statement to fetch all todos.
  • Defines a callback function that will be called for each todo in the result.
  • The callback dynamically allocates memory to store the todos.
  • Returns the array of todos and the count.

**5. todo_free_list**:

  • Takes a pointer to a todo array.
  • Frees the memory allocated for the array.

This implementation provides all the necessary functionality for managing todo items. It relies on the database module for actually executing SQL queries, which we’ll implement next.

Database Management

The database management module handles all interactions with the SQLite database. It provides a simple interface for initializing the database, executing SQL statements, and running queries with callbacks.

Database Header File (src/db/database.h)

Let’s create the header file for our database operations:

/**
 * @file database.h
 * @brief Provides interfaces for database operations
 */

#ifndef DATABASE_H
#define DATABASE_H
/**
 * @brief Initializes the database
 * @param db_path Path to the database file (or ":memory:" for in-memory database)
 * @return 0 on success, -1 on failure
 */
int db_init(const char* db_path);
/**
 * @brief Cleans up database resources
 */
void db_cleanup(void);
/**
 * @brief Executes an SQL statement that doesn't return results
 * @param sql The SQL statement to execute
 * @return 0 on success, -1 on failure
 */
int db_execute(const char* sql);
/**
 * @brief Executes an SQL query and processes results with a callback
 * @param sql The SQL query to execute
 * @param callback Function called for each row in the result
 * @param data User data passed to the callback
 * @return 0 on success, -1 on failure
 */
int db_query_callback(const char* sql, int (*callback)(void*, int, char**, char**), void* data);
#endif /* DATABASE_H */

This header declares four functions:

  1. **db_init**: Initializes the database connection. It takes a path to the database file, or ":memory:" for an in-memory database (which we'll use for testing).
  2. **db_cleanup**: Cleans up database resources when we're done.
  3. **db_execute**: Executes an SQL statement that doesn't return results (like INSERT, UPDATE, DELETE).
  4. **db_query_callback**: Executes an SQL query and processes the results with a callback function. This is used for SELECT statements.

Database Implementation File (src/db/database.c)

Now, let’s implement these functions:

/**
 * @file database.c
 * @brief Implements the database operations
 */

#include "database.h"
#include <stdio.h>
#include <sqlite3.h>
/**
 * @brief The SQLite database handle
 */
static sqlite3* db = NULL;
/**
 * @brief Initializes the database
 * @param db_path Path to the database file (or ":memory:" for in-memory database)
 * @return 0 on success, -1 on failure
 */
int db_init(const char* db_path) {
    // Open the database connection
    int rc = sqlite3_open(db_path, &db);
    if (rc != SQLITE_OK) {
        fprintf(stderr, "Cannot open database: %s\n", sqlite3_errmsg(db));
        sqlite3_close(db);
        return -1;
    }
    // Create the todos table if it doesn't exist
    const char* sql = "CREATE TABLE IF NOT EXISTS todos ("
                     "id INTEGER PRIMARY KEY AUTOINCREMENT,"
                     "title TEXT NOT NULL,"
                     "description TEXT,"
                     "completed INTEGER DEFAULT 0,"
                     "created_at INTEGER,"
                     "updated_at INTEGER"
                     ");";
    return db_execute(sql);
}
/**
 * @brief Cleans up database resources
 */
void db_cleanup(void) {
    if (db) {
        sqlite3_close(db);
        db = NULL;
    }
}
/**
 * @brief Executes an SQL statement that doesn't return results
 * @param sql The SQL statement to execute
 * @return 0 on success, -1 on failure
 */
int db_execute(const char* sql) {
    char* err_msg = NULL;
    // Execute the SQL statement
    int rc = sqlite3_exec(db, sql, NULL, NULL, &err_msg);
    // Check for errors
    if (rc != SQLITE_OK) {
        fprintf(stderr, "SQL error: %s\n", err_msg);
        sqlite3_free(err_msg);
        return -1;
    }
    return 0;
}
/**
 * @brief Executes an SQL query and processes results with a callback
 * @param sql The SQL query to execute
 * @param callback Function called for each row in the result
 * @param data User data passed to the callback
 * @return 0 on success, -1 on failure
 */
int db_query_callback(const char* sql, int (*callback)(void*, int, char**, char**), void* data) {
    char* err_msg = NULL;
    // Execute the SQL query with the callback
    int rc = sqlite3_exec(db, sql, callback, data, &err_msg);
    // Check for errors
    if (rc != SQLITE_OK) {
        fprintf(stderr, "SQL error: %s\n", err_msg);
        sqlite3_free(err_msg);
        return -1;
    }
    return 0;
}

Let’s analyze the implementation:

  1. **db_init**:
  • Opens a connection to the SQLite database using the provided path.
  • If the connection fails, it prints an error message and returns -1.
  • Creates a todos table if it doesn't exist, with columns for all our todo properties.
  • Returns the result of creating the table.

**2. db_cleanup**:

  • Closes the database connection if it’s open.
  • Sets the database handle to NULL to prevent further use.

**3. db_execute**:

  • Executes an SQL statement using sqlite3_exec.
  • This is used for statements that don’t return results (INSERT, UPDATE, DELETE).
  • If an error occurs, it prints the error message and returns -1.
  • Otherwise, it returns 0 to indicate success.

**4. db_query_callback**:

  • Executes an SQL query using sqlite3_exec with a callback function.
  • This is used for SELECT statements that return results.
  • The callback is called once for each row in the result set.
  • If an error occurs, it prints the error message and returns -1.
  • Otherwise, it returns 0 to indicate success.

Understanding SQLite Callbacks

The SQLite callback mechanism is a powerful way to process query results. When you execute a SELECT query with sqlite3_exec, SQLite calls your callback function for each row in the result set. Let's look at how this works:

int callback(void* data, int argc, char** argv, char** azColName) {
    // data: User-provided data (passed as the 4th argument to sqlite3_exec)
    // argc: Number of columns in the result
    // argv: Array of strings representing column values
    // azColName: Array of strings representing column names
    // Process the row data...
    return 0; // Continue processing (return non-zero to abort)
}

This callback pattern is used in both todo_get and todo_list to process query results. In todo_get, the callback populates a single todo structure. In todo_list, the callback adds each todo to a dynamically growing array.

Now that we have our database management module, we can move on to implementing the HTTP request handlers.

HTTP Request Handling

The HTTP request handling module is responsible for processing incoming HTTP requests and sending appropriate responses. This is where we implement the RESTful interface for our Todo API.

We’ll divide this module into two parts:

  1. Server: Handles starting and stopping the HTTP server, and routing requests to handlers.
  2. Handlers: Processes specific API endpoints and performs the requested operations.

HTTP Handlers Header File (src/http/handlers.h)

Let’s create the header file for our HTTP handlers:

/**
 * @file handlers.h
 * @brief Defines HTTP request handlers for the Todo API
 */

#ifndef HANDLERS_H
#define HANDLERS_H
#include <curl/curl.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
 * @struct ResponseData
 * @brief Structure to store HTTP response data
 */
struct ResponseData {
    char* data;   /**< Response data content */
    size_t size;  /**< Size of the response data */
};
/**
 * @brief Handles GET /todos request (list all todos)
 * @param curl CURL handle (unused, kept for consistency)
 * @param response Pointer to store the response data
 */
void handle_list_todos(CURL* curl, struct ResponseData* response);
/**
 * @brief Handles GET /todos/:id request (get a specific todo)
 * @param curl CURL handle (unused, kept for consistency)
 * @param id ID of the todo to retrieve
 * @param response Pointer to store the response data
 */
void handle_get_todo(CURL* curl, int id, struct ResponseData* response);
/**
 * @brief Handles POST /todos request (create a new todo)
 * @param curl CURL handle (unused, kept for consistency)
 * @param post_data The POST data containing the todo details
 * @param post_size Size of the POST data
 * @param response Pointer to store the response data
 */
void handle_create_todo(CURL* curl, const char* post_data, size_t post_size, struct ResponseData* response);
/**
 * @brief Handles PUT /todos/:id request (update a todo)
 * @param curl CURL handle (unused, kept for consistency)
 * @param id ID of the todo to update
 * @param post_data The PUT data containing the updated todo details
 * @param post_size Size of the PUT data
 * @param response Pointer to store the response data
 */
void handle_update_todo(CURL* curl, int id, const char* post_data, size_t post_size, struct ResponseData* response);
/**
 * @brief Handles DELETE /todos/:id request (delete a todo)
 * @param curl CURL handle (unused, kept for consistency)
 * @param id ID of the todo to delete
 * @param response Pointer to store the response data
 */
void handle_delete_todo(CURL* curl, int id, struct ResponseData* response);
#ifdef __cplusplus
}
#endif
#endif /* HANDLERS_H */

This header defines the handler functions for each of our API endpoints:

  1. **handle_list_todos**: Handles GET /todos request (list all todos)
  2. **handle_get_todo**: Handles GET /todos/:id request (get a specific todo)
  3. **handle_create_todo**: Handles POST /todos request (create a new todo)
  4. **handle_update_todo**: Handles PUT /todos/:id request (update a todo)
  5. **handle_delete_todo**: Handles DELETE /todos/:id request (delete a todo)

We also define a ResponseData structure to store the HTTP response data.

HTTP Handlers Implementation File (src/http/handlers.c)

Now, let’s implement these handler functions:

/**
 * @file handlers.c
 * @brief Implements HTTP request handlers for the Todo API
 */

#include "handlers.h"
#include "../core/todo.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <jansson.h>
#include <curl/curl.h>
/**
 * @brief Handles GET /todos request (list all todos)
 * @param curl CURL handle (unused, kept for consistency)
 * @param response Pointer to store the response data
 */
void handle_list_todos(CURL* curl, struct ResponseData* response) {
    (void)curl;  // Unused parameter, suppress warning
    todo_t* todos = NULL;
    int count = 0;
    if (todo_list(&todos, &count) == 0) {
        // Create a JSON array to hold the todos
        json_t* root = json_array();
        // Add each todo to the array
        for (int i = 0; i < count; i++) {
            json_t* todo = json_object();
            json_object_set_new(todo, "id", json_integer(todos[i].id));
            json_object_set_new(todo, "title", json_string(todos[i].title));
            json_object_set_new(todo, "description", json_string(todos[i].description));
            json_object_set_new(todo, "completed", json_boolean(todos[i].completed));
            json_object_set_new(todo, "created_at", json_integer(todos[i].created_at));
            json_object_set_new(todo, "updated_at", json_integer(todos[i].updated_at));
            json_array_append_new(root, todo);
        }
        // Convert the JSON to a string
        response->data = json_dumps(root, JSON_INDENT(2));
        response->size = strlen(response->data);
        // Clean up
        json_decref(root);
        todo_free_list(todos);
    } else {
        // Return an error message
        response->data = strdup("{\"error\": \"Failed to list todos\"}");
        response->size = strlen(response->data);
    }
}
/**
 * @brief Handles GET /todos/:id request (get a specific todo)
 * @param curl CURL handle (unused, kept for consistency)
 * @param id ID of the todo to retrieve
 * @param response Pointer to store the response data
 */
void handle_get_todo(CURL* curl, int id, struct ResponseData* response) {
    (void)curl;  // Unused parameter, suppress warning
    todo_t todo;
    if (todo_get(id, &todo) == 0) {
        // Create a JSON object for the todo
        json_t* root = json_object();
        json_object_set_new(root, "id", json_integer(todo.id));
        json_object_set_new(root, "title", json_string(todo.title));
        json_object_set_new(root, "description", json_string(todo.description));
        json_object_set_new(root, "completed", json_boolean(todo.completed));
        json_object_set_new(root, "created_at", json_integer(todo.created_at));
        json_object_set_new(root, "updated_at", json_integer(todo.updated_at));
        // Convert the JSON to a string
        response->data = json_dumps(root, JSON_INDENT(2));
        response->size = strlen(response->data);
        // Clean up
        json_decref(root);
    } else {
        // Return an error message
        response->data = strdup("{\"error\": \"Todo not found\"}");
        response->size = strlen(response->data);
    }
}
/**
 * @brief Handles POST /todos request (create a new todo)
 * @param curl CURL handle (unused, kept for consistency)
 * @param post_data The POST data containing the todo details
 * @param post_size Size of the POST data
 * @param response Pointer to store the response data
 */
void handle_create_todo(CURL* curl, const char* post_data, size_t post_size, struct ResponseData* response) {
    (void)curl;  // Unused parameter, suppress warning
    printf("DEBUG: handle_create_todo received %zu bytes of post data\n", post_size);
    if (post_data) {
        printf("DEBUG: post_data = '%.*s'\n", (int)post_size, post_data);
    } else {
        printf("DEBUG: post_data is NULL\n");
    }
    if (post_data && post_size > 0) {
        // Parse the JSON request
        json_error_t error;
        json_t* root = json_loadb(post_data, post_size, 0, &error);
        if (root && json_is_object(root)) {
            printf("DEBUG: Successfully parsed JSON\n");
            // Extract the title and description
            const char* title = json_string_value(json_object_get(root, "title"));
            const char* description = json_string_value(json_object_get(root, "description"));
            printf("DEBUG: title = %s, description = %s\n", 
                   title ? title : "NULL", 
                   description ? description : "NULL");
            if (title && description) {
                // Create the todo
                if (todo_create(title, description) == 0) {
                    printf("DEBUG: Todo created successfully\n");
                    response->data = strdup("{\"status\": \"Todo created successfully\"}");
                } else {
                    printf("DEBUG: Failed to create todo\n");
                    response->data = strdup("{\"error\": \"Failed to create todo\"}");
                }
            } else {
                printf("DEBUG: Invalid request data\n");
                response->data = strdup("{\"error\": \"Invalid request data\"}");
            }
            // Clean up
            json_decref(root);
        } else {
            printf("DEBUG: Invalid JSON data. Error: %s\n", error.text);
            response->data = strdup("{\"error\": \"Invalid JSON data\"}");
        }
    } else {
        printf("DEBUG: No data received\n");
        response->data = strdup("{\"error\": \"No data received\"}");
    }
    response->size = strlen(response->data);
}
/**
 * @brief Handles PUT /todos/:id request (update a todo)
 * @param curl CURL handle (unused, kept for consistency)
 * @param id ID of the todo to update
 * @param post_data The PUT data containing the updated todo details
 * @param post_size Size of the PUT data
 * @param response Pointer to store the response data
 */
void handle_update_todo(CURL* curl, int id, const char* post_data, size_t post_size, struct ResponseData* response) {
    (void)curl;  // Unused parameter, suppress warning
    printf("DEBUG: handle_update_todo received %zu bytes of post data\n", post_size);
    if (post_data) {
        printf("DEBUG: post_data = '%.*s'\n", (int)post_size, post_data);
    } else {
        printf("DEBUG: post_data is NULL\n");
    }
    if (post_data && post_size > 0) {
        // Parse the JSON request
        json_error_t error;
        json_t* root = json_loadb(post_data, post_size, 0, &error);
        if (root && json_is_object(root)) {
            printf("DEBUG: Successfully parsed JSON\n");
            // Extract the title, description, and completed status
            const char* title = json_string_value(json_object_get(root, "title"));
            const char* description = json_string_value(json_object_get(root, "description"));
            json_t* completed_json = json_object_get(root, "completed");
            int completed = completed_json ? json_boolean_value(completed_json) : 0;
            printf("DEBUG: title = %s, description = %s, completed = %d\n", 
                   title ? title : "NULL", 
                   description ? description : "NULL",
                   completed);
            if (title && description) {
                // Update the todo
                if (todo_update(id, title, description, completed) == 0) {
                    printf("DEBUG: Todo updated successfully\n");
                    response->data = strdup("{\"status\": \"Todo updated successfully\"}");
                } else {
                    printf("DEBUG: Failed to update todo\n");
                    response->data = strdup("{\"error\": \"Failed to update todo\"}");
                }
            } else {
                printf("DEBUG: Invalid request data\n");
                response->data = strdup("{\"error\": \"Invalid request data\"}");
            }
            // Clean up
            json_decref(root);
        } else {
            printf("DEBUG: Invalid JSON data. Error: %s\n", error.text);
            response->data = strdup("{\"error\": \"Invalid JSON data\"}");
        }
    } else {
        printf("DEBUG: No data received\n");
        response->data = strdup("{\"error\": \"No data received\"}");
    }
    response->size = strlen(response->data);
}
/**
 * @brief Handles DELETE /todos/:id request (delete a todo)
 * @param curl CURL handle (unused, kept for consistency)
 * @param id ID of the todo to delete
 * @param response Pointer to store the response data
 */
void handle_delete_todo(CURL* curl, int id, struct ResponseData* response) {
    (void)curl;  // Unused parameter, suppress warning
    // Delete the todo
    if (todo_delete(id) == 0) {
        response->data = strdup("{\"status\": \"Todo deleted successfully\"}");
    } else {
        response->data = strdup("{\"error\": \"Failed to delete todo\"}");
    }
    response->size = strlen(response->data);
}

Let’s analyze the implementation of each handler:

  1. **handle_list_todos**:
  • Calls todo_list to get all todos from the database.
  • Creates a JSON array using jansson.
  • Adds each todo as a JSON object in the array.
  • Converts the JSON to a string with json_dumps.
  • Sets the response data and size.
  • Frees resources with json_decref and todo_free_list.
  • Returns an error message if the list operation fails.

**2. handle_get_todo**:

  • Calls todo_get to retrieve a specific todo by ID.
  • Creates a JSON object for the todo.
  • Converts the JSON to a string.
  • Sets the response data and size.
  • Returns an error message if the get operation fails.

**3. handle_create_todo**:

  • Parses the POST data as JSON using json_loadb.
  • Extracts the title and description from the JSON.
  • Calls todo_create to create a new todo.
  • Returns a success or error message based on the result.
  • Includes debug prints to help with troubleshooting.

**4. handle_update_todo**:

  • Parses the PUT data as JSON.
  • Extracts the title, description, and completed status.
  • Calls todo_update to update the todo.
  • Returns a success or error message based on the result.
  • Includes debug prints for troubleshooting.

**5. handle_delete_todo**:

  • Calls todo_delete to delete the todo with the specified ID.
  • Returns a success or error message based on the result.

Understanding Jansson for JSON Processing

Jansson is a C library for encoding, decoding, and manipulating JSON data. It provides functions for:

  1. Creating JSON values:
  • json_object(): Creates a new JSON object.
  • json_array(): Creates a new JSON array.
  • json_string(const char*): Creates a new JSON string.
  • json_integer(int): Creates a new JSON integer.
  • json_boolean(int): Creates a new JSON boolean.

2. Manipulating JSON objects and arrays:

  • json_object_set_new(json_t* obj, const char* key, json_t* value): Adds a value to an object.
  • json_array_append_new(json_t* array, json_t* value): Adds a value to an array.

3. Retrieving JSON values:

  • json_object_get(const json_t* obj, const char* key): Gets a value from an object.
  • json_string_value(const json_t* str): Gets the string value of a JSON string.
  • json_integer_value(const json_t* integer): Gets the integer value of a JSON integer.
  • json_boolean_value(const json_t* boolean): Gets the boolean value of a JSON boolean.

4. Parsing and generating JSON:

  • json_loads(const char* input, size_t flags, json_error_t* error): Parses a JSON string.
  • json_loadb(const char* buffer, size_t buflen, size_t flags, json_error_t* error): Parses a JSON buffer.
  • json_dumps(const json_t* json, size_t flags): Converts JSON to a string.

5. Memory management:

  • json_decref(json_t* json): Decreases the reference count of a JSON value and frees it if the count reaches zero.

These functions are used extensively in our handler implementations to convert between C data structures and JSON.

Implementing the API

Now, let’s dive into the implementation details of each component.

Creating the Todo Data Structure

The todo structure implementation in src/core/todo.c includes functions for all CRUD operations:

// src/core/todo.c
#include "todo.h"
#include "../db/database.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

int todo_create(const char* title, const char* description) {
    time_t now = time(NULL);
    char sql[2048];
    snprintf(sql, sizeof(sql), 
             "INSERT INTO todos (title, description, completed, created_at, updated_at) "
             "VALUES ('%s', '%s', 0, %ld, %ld)",
             title, description, now, now);
    return db_execute(sql);
}
int todo_get(int id, todo_t* todo) {
    char sql[256];
    snprintf(sql, sizeof(sql), "SELECT * FROM todos WHERE id = %d", id);
    // Define a callback to populate the todo struct
    int callback(void* data, int argc, char** argv, char** azColName) {
        todo_t* t = (todo_t*)data;
        t->id = atoi(argv[0]);
        strncpy(t->title, argv[1], sizeof(t->title)-1);
        strncpy(t->description, argv[2], sizeof(t->description)-1);
        t->completed = atoi(argv[3]);
        t->created_at = atol(argv[4]);
        t->updated_at = atol(argv[5]);
        return 0;
    }
    // Zero out the todo struct first
    memset(todo, 0, sizeof(todo_t));
    // Execute the query with the callback
    int result = db_query_callback(sql, callback, todo);
    // If no rows were returned, return an error
    if (todo->id == 0) {
        return -1;
    }
    return result;
}
// Implementation of other todo functions...

This code uses our database interface to execute SQL queries and callbacks to process results.

Setting Up the Database

Our database implementation in src/db/database.c wraps SQLite operations:

// src/db/database.c
#include "database.h"
#include <stdio.h>
#include <sqlite3.h>

static sqlite3* db = NULL;
int db_init(const char* db_path) {
    int rc = sqlite3_open(db_path, &db);
    if (rc != SQLITE_OK) {
        fprintf(stderr, "Cannot open database: %s\n", sqlite3_errmsg(db));
        sqlite3_close(db);
        return -1;
    }
    // Create todos table if it doesn't exist
    const char* sql = "CREATE TABLE IF NOT EXISTS todos ("
                     "id INTEGER PRIMARY KEY AUTOINCREMENT,"
                     "title TEXT NOT NULL,"
                     "description TEXT,"
                     "completed INTEGER DEFAULT 0,"
                     "created_at INTEGER,"
                     "updated_at INTEGER"
                     ");";
    return db_execute(sql);
}
void db_cleanup(void) {
    if (db) {
        sqlite3_close(db);
        db = NULL;
    }
}
int db_execute(const char* sql) {
    char* err_msg = NULL;
    int rc = sqlite3_exec(db, sql, NULL, NULL, &err_msg);
    if (rc != SQLITE_OK) {
        fprintf(stderr, "SQL error: %s\n", err_msg);
        sqlite3_free(err_msg);
        return -1;
    }
    return 0;
}
int db_query_callback(const char* sql, int (*callback)(void*, int, char**, char**), void* data) {
    char* err_msg = NULL;
    int rc = sqlite3_exec(db, sql, callback, data, &err_msg);
    if (rc != SQLITE_OK) {
        fprintf(stderr, "SQL error: %s\n", err_msg);
        sqlite3_free(err_msg);
        return -1;
    }
    return 0;
}

This code provides a simple interface to SQLite, handling database initialization, cleanup, and query execution.

Implementing HTTP Request Handlers

Now, let’s look at how we handle HTTP requests in src/http/handlers.c:

// src/http/handlers.c
#include "handlers.h"
#include "../core/todo.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <jansson.h>
#include <curl/curl.h>

void handle_list_todos(CURL* curl, struct ResponseData* response) {
    (void)curl;  // Unused parameter
    todo_t* todos = NULL;
    int count = 0;
    if (todo_list(&todos, &count) == 0) {
        json_t* root = json_array();
        for (int i = 0; i < count; i++) {
            json_t* todo = json_object();
            json_object_set_new(todo, "id", json_integer(todos[i].id));
            json_object_set_new(todo, "title", json_string(todos[i].title));
            json_object_set_new(todo, "description", json_string(todos[i].description));
            json_object_set_new(todo, "completed", json_boolean(todos[i].completed));
            json_object_set_new(todo, "created_at", json_integer(todos[i].created_at));
            json_object_set_new(todo, "updated_at", json_integer(todos[i].updated_at));
            json_array_append_new(root, todo);
        }
        response->data = json_dumps(root, JSON_INDENT(2));
        response->size = strlen(response->data);
        json_decref(root);
        todo_free_list(todos);
    } else {
        response->data = strdup("{\"error\": \"Failed to list todos\"}");
        response->size = strlen(response->data);
    }
}
// Implementation of other handler functions...

This code converts between our C structures and JSON using libjansson. Each handler implements one of the API operations.

Building the Server

The server implementation in src/http/server.c uses libmicrohttpd to handle HTTP requests:

// src/http/server.c
#include "server.h"
#include "handlers.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#include <microhttpd.h>

static struct MHD_Daemon* http_daemon = NULL;
#define MAX_POST_DATA_SIZE 16384  // 16KB max post size
struct ConnectionInfo {
    char* post_data;
    size_t post_data_size;
    int post_data_processed;
};
static void free_connection_info(struct ConnectionInfo* con_info) {
    if (con_info) {
        if (con_info->post_data) {
            free(con_info->post_data);
        }
        free(con_info);
    }
}
static enum MHD_Result handle_request(void* cls,
                        struct MHD_Connection* connection,
                        const char* url,
                        const char* method,
                        const char* version,
                        const char* upload_data,
                        size_t* upload_data_size,
                        void** con_cls) {
    // Implementation of request handling logic...
}
int http_server_init(int port) {
    http_daemon = MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION,
                            port,
                            NULL,
                            NULL,
                            (MHD_AccessHandlerCallback)&handle_request,
                            NULL,
                            MHD_OPTION_END);
    return http_daemon ? 0 : -1;
}
void http_server_process(void) {
    // MHD handles requests in separate threads
}
void http_server_cleanup(void) {
    if (http_daemon) {
        MHD_stop_daemon(http_daemon);
        http_daemon = NULL;
    }
}

This code sets up a libmicrohttpd server that listens for HTTP requests and routes them to the appropriate handlers.

Building and Testing

Now that we have implemented all the components of our Todo API, let’s build and test it. We’ll use CMake to manage the build process, write unit tests for the core functionality, and create a shell script to test the API endpoints.

Building with CMake

CMake is a cross-platform build system that generates build files for various platforms and build tools. We’ll use it to create Makefiles for our project.

Main CMakeLists.txt

Let’s create the main CMakeLists.txt file in the project root:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(todo_rest_api C)

# Set C standard
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Set build type if not specified
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()
# Add compiler warnings
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wpedantic")
endif()
# Find required packages
find_package(CURL REQUIRED)
find_package(SQLite3 REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(JANSSON REQUIRED jansson)
# Find libmicrohttpd
find_path(MICROHTTPD_INCLUDE_DIR microhttpd.h)
find_library(MICROHTTPD_LIBRARY microhttpd)
if(NOT MICROHTTPD_INCLUDE_DIR OR NOT MICROHTTPD_LIBRARY)
    message(FATAL_ERROR "libmicrohttpd not found. Please install libmicrohttpd-dev package.")
endif()
# Print found packages
message(STATUS "CURL include dir: ${CURL_INCLUDE_DIRS}")
message(STATUS "CURL libraries: ${CURL_LIBRARIES}")
message(STATUS "SQLite3 include dir: ${SQLite3_INCLUDE_DIRS}")
message(STATUS "SQLite3 library: ${SQLite3_LIBRARIES}")
message(STATUS "Jansson include dir: ${JANSSON_INCLUDE_DIRS}")
message(STATUS "Jansson libraries: ${JANSSON_LIBRARIES}")
message(STATUS "MicroHTTPD include dir: ${MICROHTTPD_INCLUDE_DIR}")
message(STATUS "MicroHTTPD library: ${MICROHTTPD_LIBRARY}")
# Add subdirectories
add_subdirectory(src)
add_subdirectory(tests)
# Enable testing
enable_testing()

This main CMakeLists.txt file:

  1. Sets up the project and C standard.
  2. Adds compiler warnings.
  3. Finds the required libraries (CURL, SQLite3, Jansson, and libmicrohttpd).
  4. Adds the src and tests subdirectories.
  5. Enables testing.

Source CMakeLists.txt

Now, let’s create the CMakeLists.txt file in the src directory:

# src/CMakeLists.txt

# Core library
add_library(todo_core
    core/todo.c
)
target_include_directories(todo_core
    PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(todo_core
    PRIVATE
    todo_db
)
# Database library
add_library(todo_db
    db/database.c
)
target_include_directories(todo_db
    PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(todo_db
    PRIVATE
    SQLite::SQLite3
)
# HTTP library
add_library(todo_http
    http/server.c
    http/handlers.c
)
target_include_directories(todo_http
    PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}
    ${MICROHTTPD_INCLUDE_DIR}
    ${CURL_INCLUDE_DIRS}
    ${JANSSON_INCLUDE_DIRS}
)
target_link_libraries(todo_http
    PRIVATE
    todo_core
    todo_db
    ${MICROHTTPD_LIBRARY}
    ${CURL_LIBRARIES}
    ${JANSSON_LIBRARIES}
)
# Main executable
add_executable(todo_api
    main.c
)
target_link_libraries(todo_api
    PRIVATE
    todo_core
    todo_db
    todo_http
)

This CMakeLists.txt file for the source directory:

  1. Creates libraries for each component (core, database, HTTP).
  2. Sets include directories for each library.
  3. Links libraries to their dependencies.
  4. Creates the main executable and links it to the component libraries.

Tests CMakeLists.txt

Finally, let’s create the CMakeLists.txt file in the tests directory:

# tests/CMakeLists.txt

# Enable testing
enable_testing()
# Unit test executable
add_executable(test_todo
    test_todo.c
)
target_link_libraries(test_todo
    PRIVATE
    todo_core
    todo_db
)
# Add the test
add_test(
    NAME test_todo
    COMMAND test_todo
)

This CMakeLists.txt file for the tests directory:

  1. Enables testing (again, for clarity).
  2. Creates a test executable for the todo tests.
  3. Links the test executable to the core and database libraries.
  4. Adds the test to the CTest registry.

Building the Project

Now that we have our CMake files set up, we can build the project:

# Create a build directory
mkdir -p build
cd build

# Generate build files
cmake ..
# Build the project
make

If everything goes well, you should see the todo_api executable in the build directory, along with the test_todo executable in the build/tests directory.

Unit Testing

Unit tests are essential for verifying that our code works correctly. Let’s implement the test_todo.c file to test our todo operations:

/**
 * @file test_todo.c
 * @brief Unit tests for the todo operations
 */

#include "../src/core/todo.h"
#include "../src/db/database.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
/**
 * @brief Tests creating a todo
 */
void test_create_todo(void) {
    printf("Running test_create_todo...\n");
    // Initialize an in-memory database for testing
    assert(db_init(":memory:") == 0);
    // Create a test todo
    assert(todo_create("Test Todo", "Test Description") == 0);
    // Verify the todo was created correctly
    todo_t todo;
    assert(todo_get(1, &todo) == 0);
    assert(strcmp(todo.title, "Test Todo") == 0);
    assert(strcmp(todo.description, "Test Description") == 0);
    assert(todo.completed == 0);
    // Clean up
    db_cleanup();
    printf("test_create_todo passed!\n");
}
/**
 * @brief Tests updating a todo
 */
void test_update_todo(void) {
    printf("Running test_update_todo...\n");
    // Initialize an in-memory database for testing
    assert(db_init(":memory:") == 0);
    // Create a test todo
    assert(todo_create("Test Todo", "Test Description") == 0);
    // Update the todo
    assert(todo_update(1, "Updated Todo", "Updated Description", 1) == 0);
    // Verify the todo was updated correctly
    todo_t todo;
    assert(todo_get(1, &todo) == 0);
    assert(strcmp(todo.title, "Updated Todo") == 0);
    assert(strcmp(todo.description, "Updated Description") == 0);
    assert(todo.completed == 1);
    // Clean up
    db_cleanup();
    printf("test_update_todo passed!\n");
}
/**
 * @brief Tests deleting a todo
 */
void test_delete_todo(void) {
    printf("Running test_delete_todo...\n");
    // Initialize an in-memory database for testing
    assert(db_init(":memory:") == 0);
    // Create a test todo
    assert(todo_create("Test Todo", "Test Description") == 0);
    // Delete the todo
    assert(todo_delete(1) == 0);
    // Verify the todo was deleted
    todo_t todo;
    assert(todo_get(1, &todo) != 0);  // Should fail
    // Clean up
    db_cleanup();
    printf("test_delete_todo passed!\n");
}
/**
 * @brief Tests listing todos
 */
void test_list_todos(void) {
    printf("Running test_list_todos...\n");
    // Initialize an in-memory database for testing
    assert(db_init(":memory:") == 0);
    // Create test todos
    assert(todo_create("Todo 1", "Description 1") == 0);
    assert(todo_create("Todo 2", "Description 2") == 0);
    // List the todos
    todo_t* todos = NULL;
    int count = 0;
    assert(todo_list(&todos, &count) == 0);
    // Verify the todos were listed correctly
    assert(count == 2);
    assert(strcmp(todos[0].title, "Todo 1") == 0);
    assert(strcmp(todos[1].title, "Todo 2") == 0);
    // Clean up
    todo_free_list(todos);
    db_cleanup();
    printf("test_list_todos passed!\n");
}
/**
 * @brief Main entry point for the tests
 */
int main(void) {
    printf("Running todo tests...\n");
    // Run the tests
    test_create_todo();
    test_update_todo();
    test_delete_todo();
    test_list_todos();
    printf("All tests passed!\n");
    return EXIT_SUCCESS;
}

These unit tests verify that our todo operations (create, update, delete, list) work correctly. Each test:

  1. Initializes an in-memory SQLite database for testing.
  2. Performs operations on the database.
  3. Verifies that the operations were successful.
  4. Cleans up resources.

We use the assert macro to verify conditions. If any assertion fails, the program will terminate with an error message.

API Testing

In addition to unit tests, we should also test the API endpoints to ensure that they work correctly. Let’s create a shell script scripts/test_api.sh to test the API:

#!/bin/bash

# test_api.sh - Test script for the Todo API
# Set the server URL
SERVER_URL="http://localhost:8080"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to make requests and validate responses
test_request() {
    local method=$1
    local endpoint=$2
    local data=$3
    local expected_status=$4
    local description=$5
    echo -e "${BLUE}Test: ${description}${NC}"
    echo "Request: ${method} ${endpoint}"
    if [ ! -z "$data" ]; then
        echo "Data: ${data}"
    fi
    local response
    local status
    if [ "$method" == "GET" ]; then
        response=$(curl -s -w "\n%{http_code}" -X GET ${SERVER_URL}${endpoint})
    elif [ "$method" == "POST" ]; then
        response=$(curl -s -w "\n%{http_code}" -X POST -H "Content-Type: application/json" -d "${data}" ${SERVER_URL}${endpoint})
    elif [ "$method" == "PUT" ]; then
        response=$(curl -s -w "\n%{http_code}" -X PUT -H "Content-Type: application/json" -d "${data}" ${SERVER_URL}${endpoint})
    elif [ "$method" == "DELETE" ]; then
        response=$(curl -s -w "\n%{http_code}" -X DELETE ${SERVER_URL}${endpoint})
    fi
    status=$(echo "$response" | tail -n1)
    body=$(echo "$response" | sed '$d')
    echo "Response: ${body}"
    echo "Status: ${status}"
    if [ "$status" -eq "$expected_status" ]; then
        echo -e "${GREEN}✓ Test passed${NC}"
    else
        echo -e "${RED}✗ Test failed: Expected status ${expected_status}, got ${status}${NC}"
        exit 1
    fi
    echo ""
    # Return the ID of the created todo if this was a POST to /todos
    if [ "$method" == "POST" ] && [ "$endpoint" == "/todos" ]; then
        id=$(echo "$body" | grep -o '"id":[0-9]\+' | cut -d':' -f2)
        echo "$id"
    fi
}
echo -e "${BLUE}Starting API tests...${NC}"
echo "Server URL: ${SERVER_URL}"
echo ""
# Test 1: List todos (initially should be empty)
test_request "GET" "/todos" "" 200 "List todos (initially)"
# Test 2: Create a todo
todo_id=$(test_request "POST" "/todos" '{"title":"Buy groceries","description":"Get milk, bread, and eggs"}' 200 "Create a todo")
# Test 3: List todos (should contain the new todo)
test_request "GET" "/todos" "" 200 "List todos (after creation)"
# Test 4: Get a specific todo
test_request "GET" "/todos/${todo_id}" "" 200 "Get todo with ID ${todo_id}"
# Test 5: Update a todo
test_request "PUT" "/todos/${todo_id}" '{"title":"Buy groceries","description":"Get milk, bread, eggs, and cheese","completed":true}' 200 "Update todo with ID ${todo_id}"
# Test 6: Get the updated todo
test_request "GET" "/todos/${todo_id}" "" 200 "Get updated todo with ID ${todo_id}"
# Test 7: Delete a todo
test_request "DELETE" "/todos/${todo_id}" "" 200 "Delete todo with ID ${todo_id}"
# Test 8: Verify deletion
test_request "GET" "/todos/${todo_id}" "" 404 "Verify todo with ID ${todo_id} is deleted"
echo -e "${GREEN}API tests completed!${NC}"

This script tests each of our API endpoints:

  1. Lists todos (initially should be empty).
  2. Creates a new todo.
  3. Lists todos again (should contain the new todo).
  4. Gets the specific todo.
  5. Updates the todo.
  6. Gets the updated todo.
  7. Deletes the todo.
  8. Verifies that the todo has been deleted.

Running the API Tests

To run the API tests, first make sure the server is running:

# From the build directory
./todo_api

Then, in another terminal, run the API tests:

# From the project root
chmod +x scripts/test_api.sh
./scripts/test_api.sh

If everything works correctly, you should see a series of successful test results.

Debugging

If you encounter issues during testing, here are some tips for debugging:

  1. Check Error Messages: Look at error messages from the compiler, unit tests, or the server itself.
  2. Enable Debug Output: We’ve added debug print statements to the code. Make sure to check the server output for these messages.
  3. Use SQLite Command Line: You can examine the database directly using the SQLite command line tool:
  • sqlite3 todo.db
  1. Then run SQL queries like SELECT * FROM todos; to see the contents of the database.

5. Check HTTP Responses: Use tools like curl with the -v option to see detailed HTTP request and response information:

6. Memory Management: Pay attention to memory allocation and deallocation, especially in the handlers. Use tools like Valgrind to detect memory leaks.

These tools and techniques should help you identify and fix any issues you encounter.

Automating with Shell Scripts

To make development easier, we’ve created a management script manage.sh that automates common tasks. This script will provide commands for building, cleaning, running, and testing our project.

#!/bin/bash

# Script to manage the REST API project
# Usage: ./manage.sh [command]
# Commands:
#   build       - Build the project
#   clean       - Clean the build directory
#   run         - Run the server
#   test        - Run unit tests
#   api-test    - Run API tests
#   help        - Display this help message
set -e  # Exit on error
PROJECT_DIR="$(pwd)"
BUILD_DIR="${PROJECT_DIR}/build"
SRC_DIR="${PROJECT_DIR}/src"
TEST_DIR="${PROJECT_DIR}/tests"
SCRIPTS_DIR="${PROJECT_DIR}/scripts"
SERVER_PORT=8080
# Create scripts directory if it doesn't exist
mkdir -p ${SCRIPTS_DIR}
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Print colored message
print_message() {
    local color=$1
    local message=$2
    echo -e "${color}${message}${NC}"
}
# Function to build the project
build() {
    print_message "$BLUE" "Building project..."
    mkdir -p ${BUILD_DIR}
    cd ${BUILD_DIR}
    cmake ..
    make
    print_message "$GREEN" "Build complete!"
}
# Function to clean the build directory
clean() {
    print_message "$BLUE" "Cleaning build directory..."
    if [ -d ${BUILD_DIR} ]; then
        rm -rf ${BUILD_DIR}
        print_message "$GREEN" "Clean complete!"
    else
        print_message "$YELLOW" "Build directory does not exist."
    fi
}
# Function to run the server
run() {
    if [ ! -d ${BUILD_DIR} ]; then
        print_message "$YELLOW" "Build directory not found. Building first..."
        build
    fi
    print_message "$BLUE" "Starting server on port ${SERVER_PORT}..."
    cd ${BUILD_DIR}
    ./todo_api ${SERVER_PORT}
}
# Function to run unit tests
test() {
    if [ ! -d ${BUILD_DIR} ]; then
        print_message "$YELLOW" "Build directory not found. Building first..."
        build
    fi
    print_message "$BLUE" "Running unit tests..."
    cd ${BUILD_DIR}
    ctest --output-on-failure
    print_message "$GREEN" "Unit tests complete!"
}
# Function to create API test script
create_api_test_script() {
    cat > ${SCRIPTS_DIR}/test_api.sh << 'EOF'
#!/bin/bash
# API test script for TODO REST API
SERVER_URL="http://localhost:8080"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to make requests and validate responses
test_request() {
    local method=$1
    local endpoint=$2
    local data=$3
    local expected_status=$4
    local description=$5
    echo -e "${BLUE}Test: ${description}${NC}"
    echo "Request: ${method} ${endpoint}"
    if [ ! -z "$data" ]; then
        echo "Data: ${data}"
    fi
    local response
    local status
    if [ "$method" == "GET" ]; then
        response=$(curl -s -w "\n%{http_code}" -X GET ${SERVER_URL}${endpoint})
    elif [ "$method" == "POST" ]; then
        response=$(curl -s -w "\n%{http_code}" -X POST -H "Content-Type: application/json" -d "${data}" ${SERVER_URL}${endpoint})
    elif [ "$method" == "PUT" ]; then
        response=$(curl -s -w "\n%{http_code}" -X PUT -H "Content-Type: application/json" -d "${data}" ${SERVER_URL}${endpoint})
    elif [ "$method" == "DELETE" ]; then
        response=$(curl -s -w "\n%{http_code}" -X DELETE ${SERVER_URL}${endpoint})
    fi
    status=$(echo "$response" | tail -n1)
    body=$(echo "$response" | sed '$d')
    echo "Response: ${body}"
    echo "Status: ${status}"
    if [ "$status" -eq "$expected_status" ]; then
        echo -e "${GREEN}✓ Test passed${NC}"
    else
        echo -e "${RED}✗ Test failed: Expected status ${expected_status}, got ${status}${NC}"
        exit 1
    fi
    echo ""
    # Return the ID of the created todo if this was a POST to /todos
    if [ "$method" == "POST" ] && [ "$endpoint" == "/todos" ]; then
        id=$(echo "$body" | grep -o '"id":[0-9]\+' | cut -d':' -f2)
        echo "$id"
    fi
}
# Test cases
test_list_todos
test_get_todo
test_create_todo
test_update_todo
test_delete_todo
# Test 8: Verify deletion
test_request "GET" "/todos/${todo_id}" "" 404 "Verify todo with ID ${todo_id} is deleted"
echo -e "${GREEN}All tests passed!${NC}"
EOF
}
# Main script
if [ "$1" == "build" ]; then
    build
elif [ "$1" == "clean" ]; then
    clean
elif [ "$1" == "run" ]; then
    run
elif [ "$1" == "test" ]; then
    test
elif [ "$1" == "api-test" ]; then
    create_api_test_script
    ./scripts/test_api.sh
else
    echo "Usage: $0 [command]"
    echo "Commands:"
    echo "  build       - Build the project"
    echo "  clean       - Clean the build directory"
    echo "  run         - Run the server"
    echo "  test        - Run unit tests"
    echo "  api-test    - Run API tests"
    echo "  help        - Display this help message"
fi

This script provides commands for building, cleaning, running, and testing our project.

Building and Running

To build and run the project:

# Build the project
./manage.sh build

# Run the server
./manage.sh run

Testing

To run unit tests:

# Run unit tests
./manage.sh test

To run API tests (make sure the server is running first):

# In one terminal:
./manage.sh run

# In another terminal:
./manage.sh api-test

Common Issues and Debugging

When developing this API, you might encounter some common issues:

  1. Missing libraries: Make sure all dependencies are installed.
  2. Build errors: Check compilation errors carefully and fix any syntax issues.
  3. Runtime errors: Use debug print statements to trace execution.
  4. Database errors: Check SQL syntax and database connection.
  5. Memory leaks: Be careful with memory allocation and deallocation, especially in the HTTP handlers.

We’ve added debug print statements to help troubleshoot issues:

printf("DEBUG: handle_create_todo received %zu bytes of post data\n", post_size);

Next Steps

Once you have the basic API working, here are some ways to extend it:

  1. Authentication: Add user authentication with JWT or API keys.
  2. Data validation: Add more robust input validation.
  3. Pagination: Add support for paginating large result sets.
  4. Searching and filtering: Add search and filter capabilities.
  5. Docker containerization: Package the API in a Docker container for easy deployment.

Conclusion

Congratulations! You’ve built a complete RESTful API in C from scratch. This project has taught you:

  1. How to structure a C project for a web API
  2. How to work with HTTP requests and responses
  3. How to implement CRUD operations with SQLite
  4. How to process JSON data
  5. How to test your API

While C might not be the most common choice for API development, the principles you’ve learned here are applicable to any language or framework. Understanding how things work at this level will make you a better developer, even if you move on to higher-level frameworks in the future.

Remember that well-designed APIs follow REST principles, are well-documented, and have comprehensive tests. Keep these principles in mind as you develop your own APIs in the future.

You can find the complete source code for this project on GitHub

If you find this blog helpful, consider buying me a coffee


메타데이터
post_id
ab06d8e648dd
slug
building-a-restful-todo-api-in-c-a-step-by-step-guide-for-beginners-ab06d8e648dd
url
https://medium.com/@trish07/building-a-restful-todo-api-in-c-a-step-by-step-guide-for-beginners-ab06d8e648dd
canonical_url
https://medium.com/@trish07/building-a-restful-todo-api-in-c-a-step-by-step-guide-for-beginners-ab06d8e648dd
author_url
https://medium.com/@trish07
status
ok
fetched_at
2026-06-16 19:09:56