Creating Hello World web application from scratch
In this step by step tutorial, we are going to create a simple CGI web application from scratch which displays greeting message.
Who is this tutorial for?
This tutorial is written for anyone new to Fano Framework. It is assumed that you have proficiency with Free Pascal and its language syntax. You are familiar with Linux command and and its shell terminal.
Requirement
- Free Pascal >= 3.0
- git
- Apache 2.4
- Text editor, vim, VSCode, Atom etc.
- Root privilege.
Application Functionalities
-
Page which display greeting message. If query string
name=[a value]
is set, for examplename=Jon
, we will display string ‘Hello Jon’ otherwise we display ‘Hello everybody’. -
Our application should be accessible via dummy domain name
hello-world.fano
during development.
Project directory structure
For this project, we will create following directory structures
hello-world
--bin
----unit
--src
----App
------Hello
--------Controllers
----------Factories
--------Views
----------Factories
--tools
--public
----css
----js
--vendor
----fano
bin
directory will store any binary files generated by compiler.bin/unit
will store any compiled unit binaries generated by compiler.src
directory will store our application source codes.tools
directory will store helper shell scripts.public
directory is our application document root which will contains any public assets accessible from client such CSS and JavaScript files.vendor/fano
directory will contains Fano Framework source code. This will be created automatically when we setup Fano Framework as git submodule.
In this tutorial, it is assumed that absolute path of project directory is
/home/[your username]/fano-examples/hello-world
. So if you login as jon
user,
then directory is /home/jon/fano-examples/hello-world
.
Creating git repository and add Fano Framework repository
After project directory structures created (except vendor/fano
), from shell terminal, change directory to hello-world
directory and run
$ git init
$ git submodule add https://github.com/fanoframework/fano.git vendor/fano
This will initialize empty git repository and add Fano Framework to
hello-world/vendor/fano
directory.
Creating main program
Create new file hello-world/src/hello.pas
and put following code
program hello;
uses
fano,
helloapp;
var
appInstance : IWebApplication;
begin
appInstance := TCgiWebApplication.create(
THelloAppServiceProvider.create(),
THelloAppRoutes.create()
);
appInstance.run();
end.
This will be our main application which will be compiled into executable binary.
Creating hello application unit
Create new file hello-world/src/helloapp.pas
and put following code
unit helloapp;
interface
uses
fano;
type
THelloAppServiceProvider = class(TBasicAppServiceProvider)
public
procedure register(const container : IDependencyContainer); override;
end;
THelloAppRoutes = class(TRouteBuilder)
public
procedure buildRoutes(
const container : IDependencyContainer;
const router : IRouter
); override;
end;
implementation
uses
sysutils;
procedure THelloAppServiceProvider.register(const container : IDependencyContainer);
begin
//TODO: implement build application dependencies
end;
procedure THelloAppRoutes.buildRoutes(
const container : IDependencyContainer;
const router : IRouter
);
begin
//TODO: implement build application routes
end;
end.
Build application dependencies
In order to work, application needs some services to be registered into
dependency container. Add following code in register()
method
procedure THelloAppServiceProvider.register(const container : IDependencyContainer);
begin
container.add('viewParams', TViewParametersFactory.create());
end;
In code above, we register service name viewParams
, an IViewParameters
instance which we will be used to pass data from controller to view. See Displaying Data in View for more information.
Build application routes
A route is basically an association rule between request and code that handles it. Read Working with router for more information.
Add code to buildRoutes()
method to register a route.
procedure THelloAppRoutes.buildRoutes(
const container : IDependencyContainer;
const router : IRouter
);
begin
router.get('/', container.get('helloController') as IRequestHandler);
end;
So when user made GET
request to http://[our app hostname]/
, controller
registered as helloController
in dependency container will handles it.
Create hello controller
Create a file
hello-world/src/App/Hello/Controllers/HelloController.pas
and put following code
unit HelloController;
interface
uses
fano;
type
THelloController = class(TController)
public
function handleRequest(
const request : IRequest;
const response : IResponse;
const args : IRouteArgsReader
) : IResponse; override;
end;
implementation
function THelloController.handleRequest(
const request : IRequest;
const response : IResponse;
const args : IRouteArgsReader
) : IResponse;
var greetName : string;
begin
greetName := request.getQueryParam('name', 'everybody');
fViewParams.setVar('name', greetName);
result := inherited handleRequest(request, response, args);
end;
end.
It reads query string name
value or use default value of everybody
and pass them to view parameters which will get displayed in view.
So for example, http://hello-world.fano?name=jon
will cause greetName
variable to be filled with value of jon
.
fViewParams
is protected internal variable of type IViewParameters
which is defined inside TController
class.
Create hello controller factory class
Create new file
hello-world/src/App/Hello/Controllers/Factories/HelloControllerFactory.pas
And fill its content as follows
unit HelloControllerFactory;
interface
uses
fano;
type
THelloControllerFactory = class(TFactory, IDependencyFactory)
public
function build(const container : IDependencyContainer) : IDependency; override;
end;
implementation
uses
sysutils,
HelloController;
function THelloControllerFactory.build(const container : IDependencyContainer) : IDependency;
begin
result := THelloController.create(
container.get('helloView') as IView,
container.get('viewParams') as IViewParameters
);
end;
end.
This factory builds THelloController
and pass all dependencies to constructor
of the class.
Create hello view
Create new file
hello-world/src/App/Hello/Views/HelloView.pas
with content as follows
unit HelloView;
interface
uses
fano;
type
THelloView = class(TInjectableObject, IView)
public
function render(
const viewParams : IViewParameters;
const response : IResponse
) : IResponse;
end;
implementation
function THelloView.render(
const viewParams : IViewParameters;
const response : IResponse
) : IResponse;
var respBody : IResponseStream;
greet : string;
begin
greet := 'Hello ' + viewParams.getVar('name');
respBody := response.body();
respBody.write('<html><head><title>' + greet + '</title></head>');
respBody.write('<body><p>' + greet + '</p></body></html>');
result := response;
end;
end.
It read passed data from controller and build HTML of response.
Create hello view factory class
Create new file
hello-world/src/App/Hello/Views/Factories/HelloViewFactory.pas`
with content as follows
unit HelloViewFactory;
interface
uses
fano;
type
THelloViewFactory = class(TFactory)
public
function build(const container : IDependencyContainer) : IDependency; override;
end;
implementation
uses
SysUtils,
HelloView;
function THelloViewFactory.build(const container : IDependencyContainer) : IDependency;
begin
result := THelloView.create();
end;
end.
Register controller and view to dependency container
Edit register()
method in file
hello-world/src/helloapp.pas
to become
procedure THelloAppServiceProvider.register(const container : IDependencyContainer);
begin
container.add('viewParams', TViewParametersFactory.create());
container.add('helloController', THelloControllerFactory.create());
container.add('helloView', THelloViewFactory.create());
end;
Also add HelloViewFactory.pas
and HelloControllerFactory.pas
unit to uses
clause of helloapp.pas
unit.
uses
SysUtils,
HelloControllerFactory,
HelloViewFactory;
Final hello application unit
This is final content of our helloapp.pas
unit
unit helloapp;
interface
uses
fano;
type
THelloAppServiceProvider = class(TBasicAppServiceProvider)
public
procedure register(const container : IDependencyContainer); override;
end;
THelloAppRoutes = class(TRouteBuilder)
public
procedure buildRoutes(
const container : IDependencyContainer;
const router : IRouter
); override;
end;
implementation
uses
sysutils;
procedure THelloAppServiceProvider.register(const container : IDependencyContainer);
begin
container.add('viewParams', TViewParametersFactory.create());
container.add('helloController', THelloControllerFactory.create());
container.add('helloView', THelloViewFactory.create());
end;
procedure THelloAppRoutes.buildRoutes(
const container : IDependencyContainer;
const router : IRouter
);
begin
router.get('/', container.get('helloController') as IRequestHandler);
end;
end.
Creating build script
This is an optional task, but helps simplify task to compile and build application
because vendor/fano/fano.cfg
contains some environment variables need to be set.
Create new file hello-world/build.sh
#!/bin/bash
export FANO_DIR="vendor/fano"
export USER_APP_DIR="src"
export UNIT_OUTPUT_DIR="bin/unit"
export EXEC_OUTPUT_DIR="public"
export EXEC_OUTPUT_NAME="hello.cgi"
export SOURCE_PROGRAM_NAME="hello.pas"
if [[ -z "${BUILD_TYPE}" ]]; then
export BUILD_TYPE="prod"
fi
fpc @vendor/fano/fano.cfg @build.cfg ${USER_APP_DIR}/${SOURCE_PROGRAM_NAME}
Add execute permission to build.sh
$ chmod +x build.sh
You may want to create shell script to simplify delete all binaries generated by Free Pascal.
Create new file tools/clean.sh
and put
#!/bin/bash
find bin/unit ! -name 'README.md' -type f -exec rm -f {} +
Save it and add execute permission to tools/clean.sh
$ chmod +x tools/clean.sh
Create application compiler configuration
Base compiler switch configurations
Create new file hello-world/build.cfg
which will contain Free Pascal compiler switches required by application. Please consult Free Pascal documentation for
more information about these compiler switches.
-Tlinux
-Fu$USER_APP_DIR$/App/Hello/Controllers
-Fu$USER_APP_DIR$/App/Hello/Controllers/Factories
-Fu$USER_APP_DIR$/App/Hello/Views
-Fu$USER_APP_DIR$/App/Hello/Views/Factories
#INCLUDE build.$BUILD_TYPE$.cfg
Above configuration is basically to generate application for Linux, add controllers and views directory and include another configuration. $USER_APP_DIR$
and $BUILD_TYPE$
is environment variable that we set in build.sh
above.
Release compiler switch configuration
Create new file hello-world/build.prod.cfg
with content as follows,
-Se
-O3
-Ooregvar
-Oodeadvalues
-CX
-XX
-Xs
This is compiler switches for creating release build which optimized aggresively.
Development compiler switches configuration
Create new file hello-world/build.dev.cfg
with content as follows,
-Sewn
-g
-gl
-gh
-Ci
-Cr
-Co
-Ct
-CR
-Sa
-vd
-Vewnh
This are compiler switches to use during development.
Create a .gitignore file
Create file hello-world/.gitignore
to ignore all binary generated by Free Pascal compiler and also you may want to ignore any files generated by your text editor. For our project, following code is suffice
public/*.cgi
bin/unit/*
!bin/README.md
!bin/unit/README.md
Final project directories and files
This is our final project directory structures and files.
hello-world
--bin
----unit
--src
----hello.pas
----helloapp.pas
----App
------Hello
--------Controllers
----------HelloController.pas
----------Factories
------------HelloControllerFactory.pas
--------Views
----------HelloView.pas
----------Factories
------------HelloViewFactory.pas
--tools
----clean.sh
--public
----css
----js
--vendor
----fano
--build.sh
--build.cfg
--build.prod.cfg
--build.dev.cfg
--.gitignore
Commit project to git
From inside hello-world
directory, add all files and commit them.
$ git add .
$ git commit -m "Initial commit"
If all go well, we are ready to compile the application.
Build application
Assuming you are inside hello-world
directory, to build application for production build, run
$ ./build.sh
or
$ BUILD_TYPE=prod ./build.sh
To build application for development build, run
$ BUILD_TYPE=dev ./build.sh
If compilation goes well, we are ready to deploy it.
Deploy application with Apache web server
We will deploy our CGI application, so it can be accessed from web browser via URL
http://hello-world.fano
. It is assume here that you are login as jon
user and our application project path is located at /home/jon/fano-example/hello-world
directory. It is assumed that you are using Apache 2.4.
Creating virtual host configuration
If you are in Debian-based distribution, create virtual host by adding Apache configuration file inside /etc/apache2/sites-available
and create a file name fano-hello-world.conf
.
$ sudo vi /etc/apache2/sites-available/fano-hello-world.conf
Enter you root password and it will open vim text editor. Put following code
<VirtualHost *:80>
ServerAdmin admin@hello-world.fano
ServerName hello-world.fano
DocumentRoot "/home/jon/fano-examples/hello-world/public"
<Directory "/home/jon/fano-examples/hello-world/public">
Options -MultiViews +ExecCGI
AllowOverride FileInfo Indexes
Require all granted
AddHandler cgi-script .cgi
DirectoryIndex hello.cgi
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ hello.cgi [L]
</Directory>
</VirtualHost>
Save it and exit. What code above does is tells Apache to allow access to http://hello-world-fano
and map it to directory
/home/jon/fano-examples/hello-world/public
For every requests to resources which is actual files and directory, such css and JavaScript files, it tells Apache to serve them, but other resources requested, pass them to our application binary.
If you are in Fedora-based distribution, you may as well just put virtual host configuration in Apache main virtual host configuration /etc/httpd/conf.d/vhost.conf
.
Enable virtual host configuration
If you are in Debian-based distribution, you need to enable virtual host configuration and tell Apache to reload configuration by running
$ sudo a2ensite fano-hello-world.conf
$ sudo service apache2 reload
If you are in Fedora-based, you only need to tells Apache to reload configuration
$ sudo systemctl reload apache2
Setup domain name
For our development purpose, we only need to add entry to /etc/hosts
files and
add following line
127.0.0.1 hello-world.fano
If you want to use real domain name and server, you may want to setup DNS record to point to machine where Apache runs.
Run it from browser
Open web browser and go to http://hello-world.fano
, if all go well, you should be greeted with page Hello everybody. If you use URL http://hello-world.fano?name=jon
, you should see page Hello jon. If you try to access resources which not exists, ERouteHandlerNotFound
exception is triggered.
Summary
In this tutorial, you have learned
- How to setup web application project directory with Fano Framework from scratch.
- How to create main application.
- How to create routes and controller and view and its related factory and,
- How to connect them with dependency container.
- How to create shell script to helps build application
- How to deploy application with Apache web server.