Building a C HTTPS Server with Docker, PostgreSQL, and a Modern Frontend: Lessons Learned

Building a C HTTPS Server with Docker, PostgreSQL, and a Modern Frontend: Lessons Learned

So I was a bit bored about doing stuff in nodejs, so I decided to explore more options, I know there is Java and .Net but to be honest I was looking outside the OOP world
In which I started to think about my game development days using C++ which to be honest was really fun, but again I didnt want to use OOP. Then I realize there is C which is not OOP but you can use a lot of tricks to organize code. The problem with C is that I was not that familiar to use large projects , in which I had to learn CMake , VCPKG and so on.
To see my results in code you can check my github repo:
C-Server Repo

1. C as a Backend Language for Web Servers

We built a web server in C, handling HTTPS connections using OpenSSL. This required careful management of memory, file I/O, and string parsing for HTTP requests. C’s performance is excellent, but error handling and resource management are manual and unforgiving.

2. Serving Static Frontend Files

The server maps HTTP GET requests to files in the frontend/ directory, serving HTML, CSS, and JS. We learned to set correct content types and handle missing files gracefully with proper HTTP status codes.

3. RESTful Endpoints and JSON

For dynamic data, we created endpoints like /product that return JSON. Using the cJSON library, we serialized C structs to JSON, making it easy for the frontend to consume data.

4. PostgreSQL Integration

We connected to PostgreSQL using libpq, running SQL queries and mapping results to C structs. Connection errors and memory allocation issues were common pitfalls, so robust error handling was essential.

5. Frontend: Modern JS and UI

The frontend uses vanilla JavaScript to fetch product data and render a responsive carousel. We learned how to structure a simple SPA without frameworks, focusing on DOM manipulation and fetch API.

6. Configuration Management

Configuration is loaded from a JSON file using cJSON, allowing easy changes to ports, credentials, and other settings without recompiling.

7. Docker for Consistency

Docker ensures the environment is consistent across machines. We wrote a Dockerfile to install all dependencies, build the project, and run the server. This made deployment and testing much easier.

8. CMake and Build Automation

CMake and shell scripts (build.sh) automate the build process, handling dependencies and platform-specific settings. This is crucial for reproducibility and onboarding new developers.

9. Security Considerations

Using HTTPS (OpenSSL) and proper error handling improves security. We learned the importance of protecting credentials and using secure defaults.

10. Challenges and Takeaways

  • Manual memory management in C is error-prone.
  • Integrating C with modern web technologies is possible but requires careful planning.
  • Docker and CMake are invaluable for cross-platform development.
  • Clear separation of concerns (frontend, backend, config) makes the project maintainable.
Conclusion:

This project was a deep dive into systems programming, web development, and DevOps. We learned how to bridge low-level C code with modern web practices, automate builds, and deploy reliably with Docker. The experience highlighted both the power and complexity of C in web contexts, and the value of robust tooling and clear architecture.

Understanding the Flow of main.c in the C Server Project

In this post, we’ll walk through the key steps and function calls in the main.c file of the C server project, explaining how each part works and interacts with other components.

1. Including Headers

The file starts by including necessary headers, such as:

  • Standard libraries (stdio.h, stdlib.h)
  • Project-specific headers for services and utilities (frontend_service.h, greet_service.h, https_service.h, product_service.h, config_utils.h, etc.)

These headers provide function declarations and data structures used throughout the program.

2. Main Function Entry Point

The main() function is the entry point of the application. Here’s what typically happens:

a. Configuration Loading

The server loads its configuration using a function from config_utils.c, such as:

config_t* config = load_config("config.json");

This reads settings (like port, SSL keys, etc.) from a JSON file.

b. Service Initialization

Each service (frontend, greet, https, product) is initialized. For example:

frontend_service_init();
product_service_init();
https_service_init();

These functions set up routes, handlers, and any required resources for each service.

c. Starting the Server

The server is started, often by calling a function that listens for incoming connections:

start_https_server(config);

This function sets up sockets, binds to the configured port, and enters a loop to handle requests.

3. Handling Requests

When a request comes in:

  • The server determines which service should handle it (e.g., frontend for HTML, greet for greetings, etc.).
  • The appropriate handler function is called, such as handle_frontend_request(), handle_greet_request(), etc.
  • The response is generated and sent back to the client.

4. Utilities and Helpers

Utility functions from math_utils.c or other helpers may be used for tasks like logging, parsing, or calculations.

5. Graceful Shutdown

On exit (e.g., via signal or error), cleanup functions are called to free resources and close sockets.

Summary
  • main.c orchestrates the startup, configuration, and running of the server.
  • It delegates work to service modules and utility functions.
  • Each step is modular, making the code maintainable and extensible.

This structure allows you to easily add new services or modify existing ones by updating the relevant service files and their initialization in main.c.

Step-by-Step: How frontend_service.c Works in the C Server Project

Let’s break down the main components and flow of the frontend_service.c file, showing how it serves frontend content and interacts with the rest of the server.

1. Including Headers

At the top, the file includes:

  • Standard libraries (stdio.h, stdlib.h, etc.)
  • Project headers (frontend_service.h, possibly config_utils.h)

These provide function prototypes and shared data structures.

2. Initialization Function

A typical function like init_frontend_service(Config *config) sets up the frontend service. This might:

  • Register routes for serving static files (HTML, CSS, JS)
  • Configure paths to the frontend directory

Example:

// Example of a route registration function
register_route("/index.html", handle_frontend_request);
register_route("/style.css", handle_frontend_request);

3. Request Handler

The core logic is in a handler function, such as handle_frontend_request(). This function:

  • Receives HTTP requests for frontend resources
  • Determines which file to serve (e.g., index.html, blog.html)
  • Reads the file from disk
  • Sends the file contents as the HTTP response

Example:

// Pseudocode for the handler
function handle_frontend_request(request) {
    file_path = "frontend" + request.path;
    if (file_exists(file_path)) {
        send_file_as_response(file_path);
    } else {
        send_404_not_found();
    }
}

4. Error Handling

If a requested file doesn’t exist, the handler sends a 404 response:

send_http_status(404, "Not Found");

5. Integration with Main Server

The frontend service is registered with the main server during initialization. When a request matches a frontend route, the server delegates it to handle_frontend_request().

6. Utilities

Helper functions may be used for:

  • Path resolution
  • MIME type detection
  • Logging
Summary
  • frontend_service.c is responsible for serving static frontend files.
  • It initializes routes and handles requests for HTML, CSS, and JS.
  • It integrates with the main server, responding to client requests for frontend resources.

This modular approach keeps frontend logic separate and maintainable, making it easy to update or expand your web interface.

Step-by-Step: How frontend_service.c Works in the C Server Project

Let’s break down the main components and flow of the frontend_service.c file, showing how it serves frontend content and interacts with the rest of the server.

1. Including Headers

At the top, the file includes:

  • Standard libraries (stdio.h, stdlib.h, etc.)
  • Project headers (frontend_service.h, possibly config_utils.h)

These provide function prototypes and shared data structures.

2. Initialization Function

A typical function like init_frontend_service(Config *config) sets up the frontend service. This might:

  • Register routes for serving static files (HTML, CSS, JS)
  • Configure paths to the frontend directory

Example:

// Example of a route registration function
register_route("/index.html", handle_frontend_request);
register_route("/style.css", handle_frontend_request);

3. Request Handler

The core logic is in a handler function, such as handle_frontend_request(). This function:

  • Receives HTTP requests for frontend resources
  • Determines which file to serve (e.g., index.html, blog.html)
  • Reads the file from disk
  • Sends the file contents as the HTTP response

Example:

// Pseudocode for the handler
function handle_frontend_request(request) {
    file_path = "frontend" + request.path;
    if (file_exists(file_path)) {
        send_file_as_response(file_path);
    } else {
        send_404_not_found();
    }
}

4. Error Handling

If a requested file doesn’t exist, the handler sends a 404 response:

send_http_status(404, "Not Found");

5. Integration with Main Server

The frontend service is registered with the main server during initialization. When a request matches a frontend route, the server delegates it to handle_frontend_request().

6. Utilities

Helper functions may be used for:

  • Path resolution
  • MIME type detection
  • Logging
Summary
  • frontend_service.c is responsible for serving static frontend files.
  • It initializes routes and handles requests for HTML, CSS, and JS.
  • It integrates with the main server, responding to client requests for frontend resources.

This modular approach keeps frontend logic separate and maintainable, making it easy to update or expand your web interface.

Step-by-Step: How product_service.c Works in the C Server Project

This post explains the main components and flow of the product_service.c file, showing how it manages product-related functionality in your server.

1. Including Headers

The file begins by including:

  • Standard libraries (stdio.h, stdlib.h, etc.)
  • Project headers (product_service.h, possibly config_utils.h)

These provide function prototypes and shared data structures for product management.

2. Initialization Function

A function like init_product_service(Config *config) sets up the product service. This might:

  • Initialize product data (from a file, database, or hardcoded)
  • Register routes or handlers for product-related requests

Example:

// Pseudocode for a route registration
register_route("/products", handle_product_list_request);
register_route("/products/{id}", handle_product_details_request);

3. Request Handler

The core logic is in a handler function, such as handle_product_request(). This function:

  • Receives HTTP requests related to products (e.g., list, details, add, delete)
  • Parses the request to determine the action
  • Interacts with product data (fetch, update, etc.)
  • Sends the appropriate response (JSON, HTML, etc.)

Example:

// Example handler logic
json_response_t* handle_product_request(request_t* req) {
    if (req->method == GET && req->path == "/products") {
        return get_all_products_as_json();
    }
    // ... other request types
    return create_error_response(400, "Bad Request");
}

4. Data Management

Product data may be managed in-memory, loaded from a file, or connected to a database. Functions handle:

  • Listing products
  • Fetching product details
  • Adding or removing products

Example:

// Function to fetch product data from a database
product_t* get_product_by_id(int id);

5. Integration with Main Server

The product service is registered with the main server during initialization. When a request matches a product route, the server delegates it to handle_product_request().

6. Error Handling

The service handles errors such as:

  • Invalid requests
  • Missing products
  • Data access issues

Appropriate error responses are sent to the client.

Summary
  • product_service.c manages product-related endpoints and data.
  • It initializes product data, handles requests, and sends responses.
  • It integrates with the main server for routing and error handling.

This modular design makes it easy to expand product features or connect to different data sources.

Comments

Popular posts from this blog

Analysis of dark patterns in League of Legends and Star Wars:Battlefront II with the usage of Loot Boxes

Kerstin's Fate developer diary

Exploring LLMs with Ollama and Llama3