A step-by-step guide to a minimal Gebweb API for Laravel developers reskilling onto Geblang. Same items/categories JSON contract as laravel-101 and fastAPI-101: JWT auth, SQLite, pagination, filters, stats, and feature tests — API-only (no /shop UI).
Audience: Laravel / PHP developers who know controllers, Form Requests, Eloquent, Sanctum, and Pest/PHPUnit, and want the same mental model on Gebweb (decorator-driven MVC on a Geblang/Go runtime).
| Laravel idea | Gebweb / this repo |
|---|---|
| Controllers + route files | @Controller / @Get / @Post / @Patch / @Delete |
auth:sanctum middleware |
@Auth + gebweb.useAuthenticator |
| Form Requests | DTOs + validators.gb (Laravel-style messages) |
| Eloquent / repositories | db.Connection + src/services/ |
paginate() |
{ items, total, skip, limit } |
| PHPUnit / Pest | geblang test + gebweb.TestClient |
/docs (Scribe / OpenAPI) |
Gebweb SwaggerUI at /docs |
Related: geblang-101 is the same API with Geblang’s stdlib web.router only (no Gebweb). This repo is the framework path. Full catalogue: § *-101 Family.
By the end you can start the API with one command, authorize in /docs, and map every folder back to a Laravel concept.
- Gebweb 1.8.4 on Geblang 1.32 REST API on port 8014
- SQLite via
db.Connection(foreign keys on) - JWT auth — register, login (form-urlencoded
username/password), Bearer on writes +/auth/me - Controllers under
src/controllers/(Laravel-style route groups) - Service layer — user, category, item modules under
src/services/ - Domain errors with
{ detail, code }responses (familiar from laravel-101 / fastAPI-101) - Pagination —
{ items, total, skip, limit }plus item filters - Stats —
GET /items/stats/summarywith per-category breakdown - 18 feature tests via
geblang test+gebweb.TestClient(no live HTTP server) - Dockerfile (
dwgebler/geblang), docker-compose, GitHub Actions CI, Makefile - OpenAPI 3.1 + SwaggerUI mounted at
/docsand/openapi.json
- Quick Start
- Project Structure
- Dependencies:
geblang.yaml - Controllers (Laravel Controllers)
- Services (thin controllers)
- Validation (Form Requests)
- JWT authentication (Sanctum)
- Database & schema
- Feature tests (PHPUnit / Pest)
- Docker & CI
- Laravel → Gebweb Mapping
- Geblang notes for Laravel folks
- Quick Reference
- *-101 Family
Pick one: Docker or local Geblang — both use port 8014, so don’t run them at the same time.
cp .env.example .env # optional
docker compose up --buildInstall a Geblang release and put geblang on your PATH, then:
cp .env.example .env
make servemake serve runs geblang install (pulls Gebweb into vendor/ from geblang.yaml) then starts the API.
Or without Compose:
make serve-dockermake test
# or without a local install:
make test-dockerThen open:
- http://localhost:8014 – API hello
- http://localhost:8014/docs – Interactive Swagger UI (register + login, then Authorize with
Bearer <token>for POST/PATCH/DELETE) - http://localhost:8014/items – JSON list
Note: Local and Docker both use SQLite by default (DB_DATABASE). See .env.example for variables.
gebweb-101/
├── src/
│ ├── main.gb # Entry: migrate + gebweb.serve (like public/index.php + artisan serve)
│ ├── app.gb # App factory: controllers, CORS, JWT authenticator
│ ├── settings.gb # Env config (like config/*.php / .env)
│ ├── domain.gb # DomainError + {detail, code} helper
│ ├── database.gb # SQLite connection + migrate / reset
│ ├── security.gb # passwordHash + JWT + CurrentUser
│ ├── validators.gb # Laravel-style validation messages
│ ├── serializers.gb # API Resources–style JSON shaping
│ ├── controllers/ # Route controllers (like app/Http/Controllers)
│ │ ├── health.gb
│ │ ├── auth.gb
│ │ ├── categories.gb
│ │ └── items.gb
│ ├── services/ # Business logic (like app/Services)
│ │ ├── user.gb
│ │ ├── category.gb
│ │ └── item.gb
│ └── support/testkit.gb # TestClient helpers (like tests/TestCase)
├── database/schema.sql # Reference schema
├── tests/feature/ # Feature tests (18)
├── geblang.yaml # Package manifest + Gebweb dependency
├── geblang.lock # Pinned Gebweb commit
├── Dockerfile
├── docker-compose.yml
├── Makefile
├── .env.example
└── .github/workflows/ci.yml
What it is: The package manifest — Geblang’s analogue of composer.json.
What we declare:
name: gebweb-101
version: 0.1.0
source: src
dependencies:
gebweb:
git: https://github.com/dwgebler/gebweb
version: "1.8.4"| Laravel / Composer | This project |
|---|---|
composer.json |
geblang.yaml |
composer.lock |
geblang.lock |
vendor/ |
vendor/ (gitignored; created by geblang install) |
composer install |
geblang install / make install |
Key idea: Anyone (or Docker) can run geblang install and get the same Gebweb commit pinned in geblang.lock.
What they are: Classes with HTTP methods. Gebweb discovers routes from decorators — closer to Symfony attributes or NestJS than Laravel’s routes/api.php + controller methods, but the job is the same: thin HTTP adapters.
| Laravel | Gebweb |
|---|---|
Route::get('/items', ...) |
@Get("") on @Controller("/items") |
Route::post(...) |
@Post("") |
Path {item} |
{id} path params bind by name |
Controller __invoke / actions |
Methods returning http.Response via this.json(...) |
Example (health — like a trivial Laravel controller):
@Controller("")
export class HealthController extends gebweb.Controller {
@Get("/")
func root(): http.Response {
return this.json({
"message": "Hello from gebweb-101",
"docs": "https://github.com/dwgebler/gebweb",
"openapi": "/docs"
});
}
@Get("/health")
func health(): http.Response {
return this.json({"status": "ok"});
}
}
App wiring (src/app.gb) registers controllers, OpenAPI info, JWT authenticator, and CORS — think RouteServiceProvider + middleware groups in one place:
let app = gebweb.app([
HealthController(),
AuthController(),
CategoriesController(),
ItemsController(),
]);
gebweb.useAuthenticator(app, CurrentUser, ...);
gebweb.use(app, gebweb.cors({ ... }));
What they are: Business logic outside HTTP — Laravel app/Services or fat models extracted into services.
| Concern | Lives in |
|---|---|
HTTP status, JSON shape, @Auth |
src/controllers/ |
| SQL, uniqueness, “category in use” | src/services/ |
{ detail, code } mapping |
src/domain.gb |
Controllers catch domain.DomainError and return the shared *-101 error shape:
{ "detail": "Item not found", "code": "ITEM_NOT_FOUND" }Same contract as laravel-101 / fastAPI-101 so SPA clients (react-101, etc.) can swap backends by port.
Laravel: $request->validate([...]) or Form Request classes with messages like “The name field is required.”
gebweb-101: validators.gb uses the same message style. Controllers call validators.validateOrRaise(data, rules) before hitting services. Failures become 422 with { "detail": "..." }.
| Laravel rule | Example here |
|---|---|
required|email|min:5 |
Register email |
required|string|min:8 |
Password |
numeric|gt:0 |
Item price |
integer|min:1|max:100 |
limit query param |
Gebweb can also validate DTOs with @Assert.* decorators; this repo keeps Laravel-style messages for parity with other *-101 backends.
This is the Sanctum / auth:api story: register a user, exchange credentials for a Bearer JWT, protect writes.
| Laravel | gebweb-101 |
|---|---|
User + Hash::make() |
users table + crypt.passwordHash |
Auth::attempt() |
services.user.authenticate |
| Sanctum / jwt-auth | HS256 JWT (JWT_SECRET) |
Authorization: Bearer {token} |
Same header |
auth:sanctum on route group |
@Auth on controller methods |
$request->user() |
CurrentUser injected into the handler |
Important Gebweb detail: import the user class with a selective import so the type name matches the authenticator:
from security import CurrentUser;
@Post("")
@Auth
func create(CurrentUser user, CategoryCreateDTO body): http.Response { ... }
(security.CurrentUser as a qualified parameter type is treated as a body DTO — use from security import CurrentUser.)
- Open http://localhost:8014/docs
POST /auth/register→{ "email": "you@example.com", "password": "password123" }POST /auth/login— username = email, password = your password (form fields, like OAuth2 password grant)- Copy
access_token - Click Authorize →
Bearer <token> - Try
POST /itemsorPOST /categories
curl:
# Register
curl -X POST http://localhost:8014/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","password":"password123"}'
# Login (form-urlencoded; username = email)
curl -X POST http://localhost:8014/auth/login \
-d "username=you@example.com&password=password123"
# Create item with token
curl -X POST http://localhost:8014/items \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{"name":"Widget","price":9.99}'GET list/show routes stay public; POST/PATCH/DELETE and GET /auth/me require JWT.
Laravel: migrations + Eloquent.
gebweb-101: database.migrate() in src/database.gb creates tables on startup (fine for a learning SQLite app). Reference DDL also lives in database/schema.sql.
| Table | Role |
|---|---|
users |
Auth (email + password hash) |
categories |
Unique name; delete blocked if items reference it (409 CATEGORY_IN_USE) |
items |
price as text/decimal; optional category_id FK |
List filters on GET /items (Laravel query scopes / when()):
| Param | Example |
|---|---|
skip / limit |
?skip=0&limit=10 |
min_price / max_price |
?min_price=10&max_price=25 |
category_id |
?category_id=2 |
name_contains |
?name_contains=widget |
Pagination envelope (Laravel paginate() simplified):
{
"items": [ ... ],
"total": 42,
"skip": 0,
"limit": 10
}What you'll use:
| Tool | Purpose |
|---|---|
geblang test |
Discovers @test methods on classes extending test.Test |
gebweb.TestClient |
In-process HTTP (like Laravel Http:: / $this->getJson) — no port bind |
support/testkit.gb |
Fresh :memory: DB + authToken() helper per test |
Run:
make test
# geblang test tests/ -v18 tests cover health, auth, categories, item list/pagination/validation, create/delete, stats, and filters — same behavioural surface as flask-101 / geblang-101.
Example pattern:
class AuthTest extends test.Test {
@test
func meRequiresAuth(): void {
let client = h.setupClient();
client.get("/auth/me").assertStatus(401);
string token = h.authToken(client);
h.getAuth(client, "/auth/me", token).assertStatus(200);
}
}
| Piece | Role |
|---|---|
Dockerfile |
FROM dwgebler/geblang:1.32.0, geblang install, serve src/main.gb |
docker-compose.yml |
Port 8014, SQLite volume |
.github/workflows/ci.yml |
geblang install → geblang check → geblang test; separate Docker build job |
Laravel parallel: Forge/Envoyer aside, this is “Dockerfile + GitHub Actions green on every push,” same idea as laravel-101’s CI.
| Laravel | gebweb-101 |
|---|---|
artisan serve / Octane |
gebweb.serve / make serve |
routes/api.php + Controllers |
@Controller + method decorators |
middleware('auth:sanctum') |
@Auth |
$request->user() |
CurrentUser parameter |
| Form Requests | DTOs + validators.gb |
| API Resources | serializers.gb |
| Eloquent | db.Connection + services |
paginate() |
{ items, total, skip, limit } |
abort(404) / custom exceptions |
domain.*Error → { detail, code } |
Hash / JWT package |
crypt.passwordHash / crypt.jwtSign |
| PHPUnit / Pest feature tests | geblang test + TestClient |
composer.json |
geblang.yaml |
| Scribe / OpenAPI | /docs + /openapi.json |
.env |
.env / settings.gb (PORT, JWT_SECRET, …) |
- Comments use
#, not//— in Geblang//is integer division. - Modules named
config/errorscollide with stdlib — this app usessettingsanddomain. :memory:SQLite uses a pooled connection: close query cursors (rows.close()/defer) before the next query, or list endpoints can deadlock.- Prefer
from security import CurrentUserfor@Authinjection (see JWT). - Sibling without the framework: geblang-101 (port 8013, stdlib router only).
| Goal | Command |
|---|---|
| Copy env | cp .env.example .env |
| Install Gebweb | make install / geblang install |
| Start app (Docker) | docker compose up --build |
| Start app (local) | make serve → http://127.0.0.1:8014 |
| Start via Docker image only | make serve-docker |
| Stop Docker | docker compose down |
| Port 8014 in use | Stop the other process / compose stack |
| Run tests | make test / make test-docker |
| Static check | make check |
| View CI runs | GitHub → Actions |
| Interactive docs | http://localhost:8014/docs |
| Repo | Port | Type | Stack |
|---|---|---|---|
| fastAPI-101 | 8000 | API-only | FastAPI, SQLAlchemy |
| django-101 | 8001 | Monolith | Django + DRF + shop |
| symfony-101 | 8002 | Monolith | Symfony + shop |
| laravel-101 | 8003 | Monolith | Laravel + shop |
| framework-x-101 | 8004 | Monolith | Framework X + shop |
| orchestr-101 | 8005 | Monolith | Orchestr + shop |
| nest-101 | 8006 | API-only | NestJS, TypeScript |
| express-101 | 8007 | API-only | Express, Vitest |
| go-101 | 8000* | API-only | Gin, GORM |
| fortran-101 | 8008 | API-only | Fortran, fpm |
| java-101 | 8009 | API-only | Spring Boot, JPA, Flyway |
| dotNet-101 | 8010 | API-only | ASP.NET Core, xUnit |
| flask-101 | 8011 | API-only | Flask, pytest |
| rails-101 | 8012 | Monolith | Rails + shop |
| geblang-101 | 8013 | API-only | Geblang stdlib router |
| gebweb-101 | 8014 | API-only | Geblang + Gebweb |
| sinatra-101 | 8015 | API-only | Sinatra, RSpec |
| * go-101 also uses port 8000 — run one backend at a time, or change port in config. |
| Repo | Platform | Stack |
|---|---|---|
| flutter-101 | Mobile / desktop | Flutter (iOS, macOS, Android) |
| react-101 | Web browser | React 19, Vite, Vitest |
| vue-101 | Web browser | Vue 3, Vite, Pinia |
| alpine-101 | Web browser | Alpine.js, Vite, Vitest |
- Laravel → Gebweb path: laravel-101 (8003) → gebweb-101 (8014) with the mapping table above
- Same API, no framework: geblang-101 (8013) vs gebweb-101 (8014)
- Compare to FastAPI tutorial style: fastAPI-101 (8000)
- Pair with a client: react-101, vue-101, alpine-101, or flutter-101
Catalogue: automica.io/learning-101