A lightweight, framework-free backend API to manage support tickets, built with raw Node.js HTTP primitives.
This project demonstrates:
- Route parsing with dynamic params (
:id) and query strings. - JSON request body parsing middleware.
- File-based persistence (
db.json) through a custom database layer. - Ticket lifecycle operations (
open -> closed -> delete). - Business rule enforcement: only closed tickets can be deleted.
- Why This Project
- What It Does
- System Architecture
- Ticket Lifecycle
- Project Structure
- Data Model
- Business Rules
- API Reference
- Run Locally
- Developer Notes
- Roadmap Ideas
This API is an excellent baseline for understanding backend fundamentals without hiding complexity behind frameworks.
It is intentionally simple, but still covers real backend concerns:
- Routing and controller orchestration.
- Request parsing and response formatting.
- Persistence and data mutation.
- Filtering and search via query parameters.
- Domain constraints and error handling.
The API manages technical support tickets with these capabilities:
- Create a ticket with equipment, issue description, and requester name.
- List tickets.
- Filter tickets by status using query params (
?status=openor?status=closed). - Update ticket details.
- Close a ticket and register its solution.
- Delete a ticket only when status is
closed.
flowchart LR
Client[HTTP Client\nInsomnia / Postman / cURL / Frontend] --> Server[Node HTTP Server\nsrc/server.js]
Server --> JSON[jsonHandler middleware\nparse request body]
JSON --> Router[routehandler middleware\nmatch method + path]
Router --> Controllers[Tickets Controllers\ncreate/index/update/updateStatus/remove]
Controllers --> DB[Database class\nsrc/database/database.js]
DB --> File[(db.json)]
sequenceDiagram
autonumber
participant C as Client
participant S as server.js
participant J as jsonHandler
participant R as routeHandler
participant T as Ticket Controller
participant D as Database
participant F as db.json
C->>S: HTTP request
S->>J: Parse body + set Content-Type JSON
J->>R: Continue pipeline
R->>R: Match method + regex path
alt Route found
R->>T: controller({ request, response, database })
T->>D: select/insert/update/remove
D->>F: read/write persisted state
T-->>C: HTTP response
else Route not found
R-->>C: 404
end
stateDiagram-v2
[*] --> Open: POST /tickets
Open --> Open: PUT /tickets/:id
Open --> Closed: PATCH /tickets/:id/close
Closed --> Closed: PUT /tickets/:id
Closed --> Deleted: DELETE /tickets/:id
Open --> Open: DELETE /tickets/:id\n(Rejected: only closed can be deleted)
.
|-- package.json
|-- README.md
`-- src
|-- server.js
|-- controllers
| `-- tickets
| |-- create.js
| |-- index.js
| |-- remove.js
| |-- update.js
| `-- updateStatus.js
|-- database
| |-- database.js
| `-- db.json
|-- middleware
| |-- jsonHandler.js
| `-- routeHandler.js
|-- routes
| |-- index.js
| `-- tickets.js
`-- utils
|-- extractQueryParams.js
`-- parseRoutePath.js
A ticket stored in db.json follows this shape:
{
"id": "uuid",
"equipment": "Keyboard",
"description": "ALT key is stuck",
"user_name": "Diego Cunha",
"status": "open",
"created_at": "2026-03-26T20:49:57.161Z",
"updated_at": "2026-03-26T20:49:57.161Z",
"solution": "optional, present when closed"
}| Field | Type | Required on create | Notes |
|---|---|---|---|
id |
string | Auto | Generated with randomUUID() |
equipment |
string | Yes | Updated via PUT /tickets/:id |
description |
string | Yes | Updated via PUT /tickets/:id |
user_name |
string | Yes | Set on create |
status |
string | Auto | open on create, closed on close |
solution |
string | No | Added when closing ticket |
created_at |
date string | Auto | Creation timestamp |
updated_at |
date string | Auto | Updated on write operations |
- New tickets are always created with status
open. - Closing a ticket (
PATCH /tickets/:id/close) sets status toclosedand recordssolution. - Deleting a ticket is only allowed when its status is
closed. - Attempting to delete an open ticket returns
400with:
{ "error": "Only closed tickets can be deleted." }Base URL: http://localhost:3333
| Method | Route | Description |
|---|---|---|
GET |
/tickets |
List all tickets |
GET |
/tickets?status=open |
List filtered tickets |
POST |
/tickets |
Create a ticket |
PUT |
/tickets/:id |
Update ticket equipment/description |
PATCH |
/tickets/:id/close |
Close ticket with a solution |
DELETE |
/tickets/:id |
Delete ticket (only if closed) |
POST /tickets
Request body:
{
"equipment": "Notebook",
"description": "Battery drains quickly",
"user_name": "Alice"
}Example:
curl -X POST http://localhost:3333/tickets \
-H "Content-Type: application/json" \
-d '{
"equipment": "Notebook",
"description": "Battery drains quickly",
"user_name": "Alice"
}'Response: 201 Created
{
"id": "f6f9964d-2adb-4a52-b4f8-b96e8f46f67b",
"equipment": "Notebook",
"description": "Battery drains quickly",
"user_name": "Alice",
"status": "open",
"created_at": "2026-03-26T22:00:00.000Z",
"updated_at": "2026-03-26T22:00:00.000Z"
}GET /tickets
curl http://localhost:3333/ticketsResponse: 200 OK
[
{
"id": "907a16df-9dd2-479c-860a-98ab33c5da9e",
"equipment": "Keyboard",
"description": "ALT key is stucked",
"user_name": "Diego Cunha",
"status": "open",
"created_at": "2026-03-26T20:49:57.161Z",
"updated_at": "2026-03-26T20:49:57.161Z"
}
]GET /tickets?status=open
curl "http://localhost:3333/tickets?status=open"Notes:
- Filtering compares lowercase ticket status to the query value.
- Use lowercase values (
open,closed) for reliable matches.
PUT /tickets/:id
Request body:
{
"equipment": "Mechanical Keyboard",
"description": "Left ALT key remains pressed"
}curl -X PUT http://localhost:3333/tickets/<ticket_id> \
-H "Content-Type: application/json" \
-d '{
"equipment": "Mechanical Keyboard",
"description": "Left ALT key remains pressed"
}'Response: 200 OK with empty body.
PATCH /tickets/:id/close
Request body:
{
"solution": "Keycap and switch replaced"
}curl -X PATCH http://localhost:3333/tickets/<ticket_id>/close \
-H "Content-Type: application/json" \
-d '{
"solution": "Keycap and switch replaced"
}'Response: 200 OK with empty body.
DELETE /tickets/:id
curl -X DELETE http://localhost:3333/tickets/<ticket_id>Success response: 200 OK
"Ticket removed successfully"If ticket is open: 400 Bad Request
{ "error": "Only closed tickets can be deleted." }- Node.js 22+ (or a modern Node.js version supporting ESM and
--watch)
npm installnpm run devThe server starts on:
http://localhost:3333
- Native Node.js HTTP server (
node:http) without Express. - Custom route parser transforms paths into regular expressions.
- Controller injection style: each controller receives
{ request, response, database }. - In-memory object synchronized to JSON file for persistence.
- There is no explicit payload validation yet (missing fields are not rejected).
- Some update/delete operations on non-existent IDs return
200with empty/success response. - Query parser expects simple
key=value&key2=value2format. - Content-Type is always set to
application/json.
- Add schema validation for all request bodies.
- Return
404for unknown IDs in update/close/delete operations. - Normalize and validate query parameters.
- Add pagination and sorting on list endpoint.
- Add automated tests (unit + integration).
- Add Docker support and environment-based config.
- Introduce logging and request correlation IDs.
- Add authentication and role-based authorization.
ISC