Published Mar 23, 2026
A tour of backend languages and frameworks through syntax
Choosing a backend stack is part technical decision, part personal taste. The best way to get a feel for a language is to read real code. Below are minimal but representative examples for 12 popular backend technologies — each one defines a route, works with data, or shows an idiomatic pattern.
Laravel (PHP)
Laravel's expressive syntax makes CRUD operations almost read like English.
// routes/web.php
Route::get('/posts', function () {
$posts = Post::where('published', true)
->orderBy('created_at', 'desc')
->get();
return view('posts.index', compact('posts'));
});
// Creating a record with mass assignment
$post = Post::create([
'title' => 'Hello World',
'body' => 'First post content.',
'published' => true,
]);Ruby on Rails
Convention over configuration — Rails keeps things minimal.
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
@posts = Post.where(published: true).order(created_at: :desc)
end
def create
@post = Post.new(post_params)
if @post.save
redirect_to @post, notice: "Post created."
else
render :new, status: :unprocessable_entity
end
end
private
def post_params
params.require(:post).permit(:title, :body, :published)
end
endNode.js (Express)
Lightweight and callback-driven — the JavaScript way.
import express from "express";
const app = express();
app.use(express.json());
const posts = [];
app.get("/posts", (req, res) => {
const published = posts.filter((p) => p.published);
res.json(published);
});
app.post("/posts", (req, res) => {
const post = { id: posts.length + 1, ...req.body };
posts.push(post);
res.status(201).json(post);
});
app.listen(3000, () => console.log("Listening on :3000"));FastAPI (Python)
Type hints drive validation, docs, and serialization automatically.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Post(BaseModel):
title: str
body: str
published: bool = False
db: list[Post] = []
@app.get("/posts")
def list_posts():
return [p for p in db if p.published]
@app.post("/posts", status_code=201)
def create_post(post: Post):
db.append(post)
return postGo
Explicit, no magic — the standard library gets you surprisingly far.
package main
import (
"encoding/json"
"net/http"
)
type Post struct {
ID int `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
Published bool `json:"published"`
}
var posts []Post
func listPosts(w http.ResponseWriter, r *http.Request) {
var published []Post
for _, p := range posts {
if p.Published {
published = append(published, p)
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(published)
}
func main() {
http.HandleFunc("/posts", listPosts)
http.ListenAndServe(":8080", nil)
}C++
Modern C++ with a lightweight HTTP library feels surprisingly clean.
#include <httplib.h>
#include <nlohmann/json.hpp>
#include <vector>
#include <string>
using json = nlohmann::json;
struct Post {
int id;
std::string title;
std::string body;
bool published;
};
int main() {
std::vector<Post> posts;
httplib::Server svr;
svr.Get("/posts", [&](const auto& req, auto& res) {
json result = json::array();
for (const auto& p : posts) {
if (p.published) {
result.push_back({
{"id", p.id},
{"title", p.title},
{"body", p.body}
});
}
}
res.set_content(result.dump(), "application/json");
});
svr.listen("0.0.0.0", 8080);
}Rust
Ownership and type safety make Rust APIs rock-solid.
use actix_web::{get, post, web, App, HttpServer, HttpResponse};
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
#[derive(Serialize, Deserialize, Clone)]
struct Post {
title: String,
body: String,
published: bool,
}
struct AppState {
posts: Mutex<Vec<Post>>,
}
#[get("/posts")]
async fn list_posts(data: web::Data<AppState>) -> HttpResponse {
let posts = data.posts.lock().unwrap();
let published: Vec<&Post> = posts.iter().filter(|p| p.published).collect();
HttpResponse::Ok().json(published)
}
#[post("/posts")]
async fn create_post(
data: web::Data<AppState>,
post: web::Json<Post>,
) -> HttpResponse {
data.posts.lock().unwrap().push(post.into_inner());
HttpResponse::Created().finish()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let state = web::Data::new(AppState {
posts: Mutex::new(vec![]),
});
HttpServer::new(move || {
App::new()
.app_data(state.clone())
.service(list_posts)
.service(create_post)
})
.bind("0.0.0.0:8080")?
.run()
.await
}C# (ASP.NET)
Strongly typed, with a rich ecosystem for enterprise backends.
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var posts = new List<Post>();
app.MapGet("/posts", () =>
posts.Where(p => p.Published).OrderByDescending(p => p.CreatedAt));
app.MapPost("/posts", ([FromBody] Post post) =>
{
posts.Add(post);
return Results.Created($"/posts/{posts.Count}", post);
});
app.Run();
record Post(string Title, string Body, bool Published, DateTime CreatedAt);Haskell
Pure functions and strong types — web servers the functional way.
{-# LANGUAGE OverloadedStrings #-}
import Web.Scotty
import Data.Aeson (ToJSON, object, (.=))
import Data.IORef
data Post = Post
{ postTitle :: String
, postBody :: String
, postPublished :: Bool
} deriving (Show)
instance ToJSON Post where
toJSON p = object
[ "title" .= postTitle p
, "body" .= postBody p
, "published" .= postPublished p
]
main :: IO ()
main = do
ref <- newIORef ([] :: [Post])
scotty 3000 $ do
get "/posts" $ do
posts <- liftIO $ readIORef ref
json $ filter postPublished posts
post "/posts" $ do
title <- param "title"
body <- param "body"
let newPost = Post title body True
liftIO $ modifyIORef ref (newPost :)
json newPostElixir (Phoenix)
Pattern matching and the actor model make concurrency natural.
defmodule BlogWeb.PostController do
use BlogWeb, :controller
alias Blog.Content
def index(conn, _params) do
posts = Content.list_published_posts()
render(conn, :index, posts: posts)
end
def create(conn, %{"post" => post_params}) do
case Content.create_post(post_params) do
{:ok, post} ->
conn
|> put_status(:created)
|> json(%{id: post.id, title: post.title})
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{errors: format_errors(changeset)})
end
end
endKotlin (Ktor)
Coroutines and DSLs give Kotlin backends a clean, async-first feel.
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.routing.*
import io.ktor.server.response.*
import io.ktor.server.request.*
import io.ktor.http.*
import kotlinx.serialization.Serializable
@Serializable
data class Post(val title: String, val body: String, val published: Boolean)
val posts = mutableListOf<Post>()
fun main() {
embeddedServer(Netty, port = 8080) {
routing {
get("/posts") {
call.respond(posts.filter { it.published })
}
post("/posts") {
val post = call.receive<Post>()
posts.add(post)
call.respond(HttpStatusCode.Created, post)
}
}
}.start(wait = true)
}Swift (Vapor)
Server-side Swift brings iOS-familiar syntax to the backend.
import Vapor
struct Post: Content {
let title: String
let body: String
let published: Bool
}
func routes(_ app: Application) throws {
var posts: [Post] = []
app.get("posts") { req -> [Post] in
posts.filter { $0.published }
}
app.post("posts") { req -> Response in
let post = try req.content.decode(Post.self)
posts.append(post)
return Response(status: .created)
}
}What stands out
A few things become obvious when you line these up:
- Type systems vary widely. Go and Rust force you to be explicit. Python and Ruby let you move fast and add types later.
- Convention vs. configuration. Rails and Laravel give you a golden path. Go and Rust hand you the building blocks.
- Concurrency models differ. Elixir's actor model, Rust's ownership, Go's goroutines, and Kotlin's coroutines each solve the same problem differently.
- Ecosystem maturity matters. The language is only part of the story — the ORM, router, and middleware ecosystem shape day-to-day productivity just as much.
Pick the one that matches how your team thinks — you can build great software in any of them.