This repository is an implementation (rework, build, etc.) of nodejs Express packed in a library to support easier server building in C. This functionality matches very closely with many functions in Express (if not quite as built out as Express currently). This README goes through the process of creating a simple web server and how each component works.
The following code is an example of a simple Teru server that sends a simple HTML file home.html
when a socket joins and goes to /
on the page:
#include <stdio.h>
#include <stdlib.h>
#include "teru.h"
#define HOST "localhost"
#define PORT "8888"
void home_page(req_t req, res_t res) {
res_end("Hi there, welcome to the home page");
}
int main() {
// initialize the server
teru_t app = teru();
// setup listener routes
app_get(app, "/", home_page);
// listen on the port and host defined above
app_listen(HOST, PORT, app);
return 0;
}
Wow! Looks pretty similar to the Express functions. Below lays out the current functions within Teru:
- res_sendFile() -- Send a file
- res_end() -- Send a string
- res_render() -- Send a file with match keys that replace to allow for dynamic HTML pages
teru()
returns a teru_t
struct which will have some default parameters set but remains mostly untouched. The functions following this will get into how to personalize Teru for your project. From the example code above, creating a new Teru instance just involves running:
teru_t app = teru();
This app
variable will be used throughout the following function examples.
The app_use
function will be how to add any extra components onto the Teru instance.
Not done
To app_set
, this takes in a key and a value pair for what to load in. For example, loading a views
directory would look like:
app_set(app, "views", getenv("PWD"), "/views/");
getenv("PWD")
is the current working directory of the server file. Then sending any files look in this directory for the file instead of the default behavior of looking in the same directory as the main program.
Currently views
is the only functionality for app_set()
, more to come in the future.
res_sendFile()
takes in the name of a file from which to read the data. This data is then sent as the body of the response to the user. If home.html
contained:
<!DOCTYPE html>
<html>
<head>
<title>Example Page</title>
</head>
<body>
<h1>Hey there!!!</h1>
</body>
</html>
Then attaching this to a listener would involve creating a simple listener that watches for requests at /
:
void home_page(req_t req, res_t res) {
res_sendFile(res, "home.html");
}
Then attaching to /
would involve using app_get()
which then handles the rest:
app_get(app, "/", home_page);
Teru is ready to say Hey there!!!
to all the interested users!
res_end()
works similarly to res_sendFile()
, but instead of a file name as the second parameter takes in a string message:
void hello_there(req_t req, res_t res) {
res_end(res, "Hello there.");
}
Then connect to an endpoint:
app_get(app, "/hi", hello_there);
This allows for a server to take an HTML page and find and replace occurences of match strings. There are a few steps for setting this up:
- Create an HTML file with match strings. The start match and end match can be whatever strings you wish. However, these strings must match what you give the
render()
function in the following steps.
<html>
<head>
<title>Render Example</title>
</head>
<body>
<h1>Hi there {{NAME}}!</h1>
</body>
</html>
- Next, set the match parameters using the
res_matches()
function, which for the previous example would look like the following. Note thatres
references the second parameter of the handler function (see res_sendFile for an example).
res_matches(res, "NAME", "charlie-map");
- Finally, use the
res_render()
function to interpret the match strings and send the result to the user.res_render()
takes inres
, the name of the file, and the start match and end match. Assuming the above HTML file is named "home.html", theres_render()
call would look like:
res_render(res, "home", "{{", "}}");
Request query parameters are added at the end of the URL (for example localhost:8888/hi?name=Charlie
). req_query()
allows access to these by inserting the name of the query parameter:
void hello_name(req_t req, res_t res) {
char *name = req_query(req, "name");
printf("name: %s\n", name); // expect "Charlie" as output
res_end(res, "Hi!");
}
Then you could easily add that name into the response string. As with previous examples, connect to Teru using app_get()
or app_post()
When using app_post()
for a specific listener, you have access to req_body()
. Taking the query example, instead you would have:
void hello_name(req_t req, res_t req) {
char *name = req_body(req, "name");
printf("name: %s\n", name);
res_end(res, "Hi!");
}
The only difference involves connecting the listener to Teru with app_post()
instead:
app_post(app, "/hi", hello_name);
Currently there are a few bucket list items that will be check off over time. However, feel free to leave an issue with any suggested enhancements.
app_put()
,app_delete()
, etc.- more
app_set()
functionality