Henning Koch
Head of Development at
makandra
@triskweline
Entire app runs on a single computer
Entire app is (often) written in a single language
Popular languages: Java, C#, C++, Objective-C
Application runs on multiple computers
Typical: One server, many clients (= web browsers)
Client and server run different parts of the application
and they communicate over HTTP
Application is written in a "stack" of many technologies
Typical stack: HTML, CSS, JavaScript, Ruby/Node/Java/C#, SQL
| Language | Purpose | Runs where? |
|---|---|---|
| HTML | Content | Client |
| CSS | Layout & Design | Client |
| JavaScript | Client-side interaction | Client |
| HTTP | Client/server communication | Client/server |
| Ruby/Node/Java/C# | Controller & Model | Server |
| SQL | Database access | Server |
http://webdev101.makandra.de
https://github.com/makandra/webdev101
<html>
<body>
<h1>Hi there!</h1>
<p>Look at this image:</p>
<img src="image.jpg">
</body>
</html>
View example
(use the inspector)
User enters into her browser's address bar:
http://makandra.com/page.html
What happens?
Browser asks server:
GET /page.html HTTP/1.1 # I want the file /page.html
Host: makandra.com # I want it from makandra.com
Accept: text/html, text/plain # I understand HTML and plain text
Server looks for a local file page.html and replies:
HTTP/1.1 200 OK # I found what you wanted
Content-Length: 114 # What you want has 114 bytes
Content-Type: text/html # And it's in HTML format
# Here it comes:
<html>
<body>
<h1>Hi there!</h1>
<p>Look at this image:</p>
<img src="image.jpg">
</body>
</html>
The browser parses the HTML and encounters this tag:
<img src="image.jpg">
What happens?
Browser asks server:
GET /image.jpg HTTP/1.1 # I want the file /image.jpg
Host: makandra.com # I want it from makandra.com
Accept: image/jpeg, image/png # I understand JPEG and PNG
Server looks for a local file image.jpg and replies:
HTTP/1.1 200 OK # I found what you wanted
Content-Length: 67840 # What you want has 66 KB
Content-Type: image/jpeg # And it's in JPEG format
# Here it comes:
FF D8 FF E0 00 10 4A 46
49 46 00 01 01 01 00 48
00 48 00 00 FF E1 1C 13
45 78 69 66 00 00 49 49
2A 00 08 00 00 00 0D 00
0F 01 02 00 06 00 00 00
...
<html>
<body>
<h1>About Capybaras</h1>
<p>
The capybara is the
largest rodent
in the world.
</p>
</body>
</html>
<html>
<body>
<h1>About Capybaras</h1>
<p>
The capybara is the
largest rodent
in the world.
</p>
</body>
</html>
body {
font-family: 'Arial';
}
h1 {
text-transform: uppercase;
font-style: italic;
}
b {
background-color: #ff9900;
}
#ff0000#00ff00#0000ff#ffff00#ffffff#000000#ff9900#4488bb#9900ff#777777#22aa88#885555redyellowfuchsiarebeccapurplergb(255 0 0 / 100%)rgb(255 0 0 / 50%)rgb(255 0 0 / 25%)hsl(0 100% 50%)hsl(30 100% 50%)hsl(30 50% 50%)hsl(30 50% 70%)oklch(62.8% 0.2577 29.23)<html>
<head>
<style>
h1 {
color: #ff0000;
}
</style>
</head>
<body>
...
</body>
</html>
<html>
<head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
...
</body>
</html>
The browser parses the HTML and encounters this tag:
<link rel="stylesheet" href="styles.css">
What happens?
Browser asks server:
GET /styles.css HTTP/1.1 # I want the file /styles.css
Host: makandra.com # I know you as makandra.com
Accept: text/css # I understand the CSS format
Server looks for a local file styles.css and replies:
HTTP/1.1 200 OK # I found what you wanted
Content-Length: 182 # What you want has 182 bytes
Content-Type: text/css # And it's in CSS format
# Here it comes:
body {
font-family: 'Arial';
}
h1 {
text-transform: uppercase;
font-style: italic;
}
...
body {
font-family: 'Arial';
}
h1 {
text-transform: uppercase;
font-style: italic;
}
b {
background-color: #ff0000;
}
body {
font-family: 'Arial';
}
h1 {
text-transform: uppercase;
font-style: italic;
}
b {
background-color: #ff0000;
}
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The <i>capybara</i> belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
h1 { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The <i>capybara</i> belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
p { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The <i>capybara</i> belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
p.introduction { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The <i>capybara</i> belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
p:last-child { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The <i>capybara</i> belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
h1, p:last-child { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The <i>capybara</i> belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
b { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The <i>capybara</i> belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
p.introduction b { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The <i>capybara</i> belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
p:has(i) { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The <i>capybara</i> belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
.card {
width: 100px;
}
.card {
width: 20em;
}
.card {
width: 50%;
}
.card {
width: calc(50% + 15px);
}
.card {
padding-top: 10px;
padding-right: 20px;
padding-bottom: 10px;
padding-left: 20px;
}
.card {
padding: 10px 20px;
}
a {
color: #ff2200;
}
.card {
border-style: solid;
border-width: 2px;
border-color: #ff2200;
}
html {
--accent: #ff2200;
}
a {
color: var(--accent);
}
.card {
border-style: solid;
border-width: 2px;
border-color: var(--accent);
}
.container {
display: block; /* default */
}
.container {
display: flex;
}
.container {
display: flex;
justify-content: flex-start; /* default */
}
.container {
display: flex;
justify-content: flex-end;
}
.container {
display: flex;
justify-content: space-between;
}
.container {
display: flex;
justify-content: space-around;
}
.container {
display: flex;
justify-content: center;
}
.container {
display: flex;
}
.child:nth-child(1) {
margin-right: auto;
}
.container {
display: flex;
}
.child {
flex-grow: 1;
}
.container {
display: flex;
}
.child:nth-child(2) {
flex-grow: 1;
}
.container {
display: flex;
}
.child:nth-child(1) {
flex-grow: 1;
}
.child:nth-child(2) {
flex-grow: 2;
}
.container {
display: flex;
}
.child {
flex-basis: 200px;
}
.container {
display: flex;
}
.child {
flex-basis: 200px;
}
.child:nth-child(2) {
flex-grow: 1;
}
.container {
display: flex;
}
.container {
display: flex;
}
.child {
flex-shrink: 0;
}
.container {
display: flex;
flex-wrap: wrap;
}
.container {
display: flex;
flex-wrap: wrap;
}
.child {
flex-basis: 25%;
}
.container {
display: flex;
flex-direction: row;
}
.container {
display: flex;
flex-direction: column;
}
.container {
display: flex;
height: 200px;
align-items: stretch; /* default */
}
.container {
display: flex;
height: 200px;
align-items: flex-start;
}
.container {
display: flex;
height: 200px;
align-items: flex-end;
}
.container {
display: flex;
height: 200px;
align-items: center;
}
.container {
display: flex;
height: 200px;
align-items: center;
}
.child:nth-child(2) {
align-self: stretch;
}
Advanced users can take a look at CSS grids.
Instead of starting from scratch you can build upon someone else's CSS.
CSS frameworks like Bootstrap, Bulma or Primer give you:
Frameworks like Tailwind do not extract components like cards or buttons. They only offer utility classes:
De-duplicate with a component system that abstracts away your HTML.
Wouldn't recommend when you're just learning CSS.
Let's write a web app that returns the current time:
http://timeapp.com/now.html
Browser asks server:
GET /now.html HTTP/1.1 # I want the file /now.html
Host: timeapp.com # I want it from timeapp.com
Accept: text/html, text/plain # I understand HTML and plain text
Server looks for a local file now.html and replies:
HTTP/1.1 200 OK # I found what you wanted
Content-Length: 22 # What you want has 22 bytes
Content-Type: text/plain # And it's in plain text
# Here it comes:
The time is 15:00:00.
Browser asks server:
GET /now.html HTTP/1.1 # I want the file /now.html
Host: timeapp.com # I want it from timeapp.com
Accept: text/html, text/plain # I understand HTML and plain text
Server runs a program time.exe.
The program time.exe writes a HTTP response
with the current time to STDOUT:
puts "HTTP/1.1 200 OK"
puts "Content-Length: 22"
puts "Content-Type: text/plain"
puts
puts "The time is " + Time.now.to_s
The browser simply makes a request and gets back a string of HTML (wrapped in HTTP).
The browser doesn't even know if the HTML was generated by an app,
or if it was simply read from a static file.
Popular choices:
The following examples are in Ruby, but concepts apply to all languages.
class User
def initialize(name, age)
@name = name
@age = age
end
def adult?
@age >= 18
end
end
user = User.new('Max', 18)
puts user.name
puts user.adult?
class User {
private String name;
private int age;
public User(name, age) {
this.name = name;
this.age = age;
}
public boolean isAdult() {
return age >= 18;
}
}
var user = new User("Max", 18);
System.out.println(user.getName());
System.out.println(user.isAdult());
To run the chat app:
cd examples/2000_chat
bundle install
bundle exec ruby app.rb
You can now access the app at http://localhost:4567/
The user types into her address bar:
http://chatapp.com/
What happens?
Browser asks server:
GET / HTTP/1.1 # I want /
Host: chatapp.com # I want it from chatapp.com
Accept: text/html, text/plain # I understand HTML and plain text
Server asks the program app.rb for /.
The program prints a new HTML page with the latest chat messages.
The server takes the program output and sends it the browser:
HTTP/1.1 200 OK # I found what you wanted
Content-Length: 1589 # What you want has 1589 bytes
Content-Type: text/html # And it's in HTML format
# Here it comes:
<html>
...
</html>
The user sends a message to the chat channel:
Hello world
What happens?
Browser asks server:
POST /send HTTP/1.1 # I'm posting data to /send
Host: chatapp.com # I'm posting it to chatapp.com
Content-Type: application/x-www-form-urlencoded # I'm posting key/value pairs
# Here it comes:
message=Hello+world
Server calls the program app.rb with the posted data.
The program stores the message and prints a HTTP response.
The server takes the program output and sends it the browser:
HTTP/1.1 303 See Other # This conversation continues elsewhere
Location: / # Request / to continue
The browser automatically makes another request to /
and gets a fresh copy of the message list.
These things require code that runs in the browser, not on the server.
// This is a comment
let x = 1
let y = x + 1
console.log(y) // 2
Try it in your browser console! (CTRL+Shift+J)
function square(n) {
return n * n
}
let x = square(16)
console.log(x) // 256
let z = square
console.log(z) // function { ... }
console.log(z(4)) // 16
one
two
three
let div = document.querySelector('div')
let children = div.children
console.log(children) // [one
, two
, three
]
let parent = children[0].parentNode
console.log(parent) // [...]
console.log(parent === div) // true
children[1].style.color = 'red'
<html>
<body>
Hi world
<script>
var x = 1;
var y = x + y;
</script>
</body>
</html>
<html>
<body>
Hi world
<script src="scripts.js"></script>
</body>
</html>
<button>Click me</button>
<script>
let button = document.querySelector('button')
button.addEventListener('click', function(event) {
alert('Button was clicked!')
})
window.addEventListener('scroll', function(event) {
alert('User scrolled!')
})
</script>
Client-side JavaScript that enhances server-rendered HTML is often called Progressive Enhancement or JavaScript Sprinkles.
null.undefined.this.foo() required to call your own methods.this.function showLastMessage() {
let lastMessage = document.querySelector('.message:last-child')
if (lastMessage) {
lastMessage.scrollIntoView()
}
}
window.addEventListener('load', showLastMessage)
⇢ User requests a website
⇠ Server sends a fresh copy
⇢ User clicks a link
⇠ Server sends a fresh copy
⇢ User submits a form
⇠ Server sends a fresh copy
⇢ User reloads the page manually
⇠ Server sends a fresh copy
⇣ Another user sends a message
× Server has no way to push the update to the user
fetch()async function refresh() {
let response = await fetch('/')
let newHTML = await response.text()
let parser = new DOMParser()
let newDocument = parser.parseFromString(newHTML, 'text/html')
let newChannel = newDocument.querySelector('.channel')
let oldChannel = document.querySelector('.channel')
oldChannel.replaceWith(newChannel)
}
setInterval(refresh, 1000)
Advanced users can take a look at WebSockets or Server-Sent Events.
To run the chat app:
cd examples/2010_chat_with_javascript
bundle install
bundle exec ruby src/app.rb
Also known as Single-page apps or SPAs.
To run the chat app:
cd examples/2020_chat_spa
bundle install
bundle exec ruby src/app.rb
| Server-side app | Client-side app | |
|---|---|---|
| Code complexity | low | high |
| Language | your choice | JavaScript |
| Data access | sync | async via API |
| Initial load time | fast | slow |
| Optimistic rendering | duplicates logic | view bound to client-side state |
| Offline | very hard | hard |
There are also hybrid solutions like
Unpoly, htmx
or React Server Components.
@triskweline
henning.koch@makandra.de
http://webdev101.makandra.de
https://github.com/makandra/webdev101
app.rb
Our current implementation has app.rb is a single script
with a lot of responsibilities:
As software engineers we always try to separate responsibilities into individual modules.
get '/' do
@messages = File.read('messages.txt').lines
erb(:channel)
end
get '/send' do
message = params['message'] + "\n"
File.write('messages.txt', message, mode: 'a')
redirect('/')
end
db = Mydb::Client.new(host: 'localhost')
get '/' do
@messages = db.select_values('SELECT text FROM messages')
erb(:channel)
end
get '/send' do
message = params['message']
escaped = db.escape(message)
db.execute('INSERT INTO messages VALUES (' + escaped + ')')
redirect('/')
end
channel = Channel.new
get '/' do
@messages = channel.messages
erb(:channel)
end
get '/send' do
message = params['message']
channel.add_message(message)
redirect('/')
end
class Channel
def initialize
@db = Mydb::Client.new
end
def messages
@db.select_values('SELECT text FROM messages')
end
def add_message(message)
escaped = @db.escape(message)
@db.execute('INSERT INTO messages VALUES (' + escaped + ')')
end
end
| Layer | Responsibility |
|---|---|
| Database | Data storage & queries |
| Model | O/R mapping, business logic |
| Controller | Layer orchestration |
| View | Content |
| Stylesheets | Optics (optional) |
| JavaScript | Behavior enhancements (optional) |
telnet example.com 80