backend-crystal
backend-crystal
A small REST API written in Crystal without any web framework. Everything — the HTTP server, the router, the layered architecture, authentication and security — is built by hand on top of Crystal's standard library, just to understand how a backend works under the hood.
Why
This is a personal project. Instead of reaching for a framework (like Kemal or Lucky), I wanted to assemble the whole thing myself: route matching, middleware, the request lifecycle, and a clean separation of concerns. It is intentionally simple but follows the same layered structure you would find in a production backend.
Features
- No framework — built directly on Crystal's
http/server. - Custom Trie-based router with support for static segments, dynamic params (
/users/read/:id) and per-route authentication flags. - Layered architecture: Route → Controller → Service → Repository → Entity.
- PostgreSQL access through the
pgshard, with DTOs to shape input/output. - JWT authentication read from a cookie, applied per route via middleware.
- Security headers (CSP, X-Frame-Options, etc.) and a small CORS handler.
- Centralized error handling with a custom
GenericExceptionand HTTP status codes. - Transaction helper that maps common PostgreSQL errors to clean API responses.
Architecture
A request flows through clearly separated layers, each with a single responsibility:
HTTP request
│
▼
app.cr ............... server setup, global error handling, security & CORS
│
▼
Router (Trie) ....... matches path, extracts params, checks auth
│
▼
Route ............... wires the endpoint to a controller action
│
▼
Controller .......... parses/validates the request, builds the response
│
▼
Service ............. business logic
│
▼
Repository .......... database queries (PostgreSQL)
│
▼
Entity / DTO ........ data models and serialization
Project structure
src/
├── app.cr # Entry point: server, error handling, wiring
├── config/
│ ├── cors.cr # CORS handling
│ ├── database.cr # PostgreSQL connection
│ ├── security.cr # Security headers
│ └── transactions.cr # Transaction helper + PG error mapping
├── controllers/ # Request/response handling
├── dtos/ # Input/output data shapes + validation
├── entities/ # Database models
├── middleware/
│ └── Auth.cr # JWT validation
├── repositories/ # Database queries
├── router/
│ ├── router.cr # Trie-based router
│ └── trie.node.cr # Trie node
├── routes/ # Endpoint definitions
├── services/ # Business logic
└── utils/
└── exception.cr # Custom exception type
API
All routes are prefixed with /api/v1.
Note: These routes are examples to demonstrate the architecture — in particular how per-route authentication is wired through the router. The auth configuration shown here is intentionally minimal and would not reflect a real-world setup (e.g.
POST /addwould normally be a protected route).
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/users/readAll |
No | List all users |
| GET | /api/v1/users/read/:id |
Yes | Get a user by id |
| POST | /api/v1/users/add |
No | Create a user |
Responses follow a simple JSON envelope:
{ "ok": 1, "data": { "id": 1, "name": "Raúl", "email": "raul@example.com" } }
Errors return the same shape with an error message and the matching HTTP status code.
Protected routes expect a JWT in a token cookie, signed with SECRET_KEY (HS256).
Requirements
- Crystal
>= 1.17.1 - PostgreSQL
Getting started
-
Install dependencies
shards install -
Create the database table
CREATE TABLE users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL ); -
Configure environment variables
Create
src/config/.env(this file is git-ignored):PORT=8080 DB_PORT=5432 DB_HOST=localhost DB_NAME=your_db DB_USER=postgres DB_PASSWORD=your_password SECRET_KEY=your_jwt_secret -
Run the server
crystal run src/app.crThe server will start on
http://localhost:8080(or thePORTyou set).
Build
shards build --release
./bin/backend-crystal
Tests
crystal spec
License
Author
- Raúl — creator and maintainer
backend-crystal
- 0
- 0
- 0
- 0
- 3
- about 10 hours ago
- February 11, 2026
MIT License
Thu, 21 May 2026 10:34:06 GMT