REST API for Journally, a personal journaling app for managing users, collections, and journal entries.
The API is built with Node.js, Express, TypeScript, Prisma, and PostgreSQL. It includes JWT authentication, request validation middleware, Swagger/OpenAPI documentation, and a WebSocket endpoint for editor autosave.
- Features
- Tech Stack
- Requirements
- Installation
- Environment Variables
- Frontend
- Available Scripts
- Database
- Swagger Documentation
- Entry WebSocket
- Observability
- Project Structure
- Testing
- ERD
- Public Repository Checklist
- User registration and login.
- JWT authentication with
x-access-tokenandx-refresh-tokenheaders. - Session renewal with 15-minute access tokens and 30-day refresh tokens.
- CRUD operations for journal entries.
- CRUD operations for collections.
- JSON-based entry descriptions, designed to store Tiptap editor content.
- Paginated lists with search and sorting support.
- Request validation with
express-validator. - Interactive API documentation with Swagger UI.
- PostgreSQL access through Prisma ORM.
- WebSocket autosave for journal entries.
- Node.js
22.x - Express
- TypeScript
- Prisma ORM
- PostgreSQL
- JSON Web Tokens
- bcrypt
- Swagger/OpenAPI
- ws
- Sentry — error tracking, tracing, and selective operational logs
- Winston — structured application and error logging
- Vercel — deployment and runtime log visibility
- Jest + Supertest
- Node.js
22.x - pnpm
- Docker Desktop (or Docker Engine with Docker Compose) for the local development database
- Environment variables configured
git clone https://github.com/SabriBere/Journally-API.git
cd Journally-API
pnpm install
cp .env.example .env.dev
pnpm db:start
pnpm generate
pnpm db:migrate:dev
pnpm devThis starts PostgreSQL in Docker, applies every committed migration, and runs
the API in watch mode. A successful startup exposes the REST API at
http://localhost:8080/api and Swagger UI at
http://localhost:8080/swagger.
Stop the local database when you finish:
pnpm db:stopLocal development example:
NODE_ENV=development
PORT=8080
SERVER=localhost
DATABASE_URL="postgresql://postgres:postgres@localhost:5433/journally_dev?schema=public"
DIRECT_URL="postgresql://postgres:postgres@localhost:5433/journally_dev?schema=public"
JWT_SECRET=replace_with_a_local_access_token_secret
JWT_REFRESH_SECRET=replace_with_a_local_refresh_token_secret
SALT_ROUND=10
ALLOWED_ORIGINS=http://localhost:3000
SENTRY_DSN=
SENTRY_TRACES_SAMPLE_RATE=0.1Production example:
NODE_ENV=production
PORT=8080
SERVER=your-api-domain.com
DATABASE_URL="postgresql://USER:PASSWORD@HOST:6543/postgres?pgbouncer=true"
DIRECT_URL="postgresql://USER:PASSWORD@HOST:5432/postgres"
JWT_SECRET=replace_with_a_secure_secret
JWT_REFRESH_SECRET=replace_with_a_secure_refresh_secret
SALT_ROUND=10
ALLOWED_ORIGINS=https://your-frontend-domain.com
SENTRY_DSN=replace_with_your_sentry_dsn
SENTRY_TRACES_SAMPLE_RATE=0.1Configuration variables:
| Variable | Purpose |
|---|---|
NODE_ENV |
Enables development-only behavior such as Swagger UI when set to development. |
PORT |
HTTP and WebSocket server port. Defaults to 8080. |
SERVER |
Hostname displayed in the local Swagger server URL. |
ALLOWED_ORIGINS |
Comma-separated frontend origins accepted by CORS. |
SALT_ROUND |
bcrypt work factor used when hashing passwords. |
DATABASE_URL |
PostgreSQL connection used by the API runtime. |
DIRECT_URL |
Direct PostgreSQL connection used by Prisma migrations. |
JWT_SECRET |
Secret used to sign 15-minute access tokens. |
JWT_REFRESH_SECRET |
Independent secret used to sign 30-day refresh tokens. |
SENTRY_DSN |
Optional Sentry project DSN. Sentry is disabled when this value is absent. |
SENTRY_TRACES_SAMPLE_RATE |
Optional Sentry trace sampling rate. Defaults to 0.1. |
The WebSocket server uses the same HTTP server and PORT as the REST API. There
is no separate socket port in the current implementation.
.env.dev, .env.prod, .env.test, and other real environment files are
ignored by Git. The committed .env.example and .env.test.example files
contain placeholders or local-only Docker credentials, which must not be reused
in production.
The companion Next.js application lives in
SabriBere/Journally-Web.
For local integration, run the frontend on an origin listed in
ALLOWED_ORIGINS and configure its NEXT_PUBLIC_API_URL as
http://localhost:8080/api.
pnpm devStarts the development server using .env.dev. Start PostgreSQL and apply
pending migrations before running it.
pnpm db:start
pnpm db:status
pnpm db:stop
pnpm db:logsStarts, inspects, stops, or follows the logs of the PostgreSQL 17 container
defined in compose.yaml. The database is available on local port 5433, and
its data is persisted in the Docker volume journally-api_postgres_data.
The first pnpm db:start downloads the PostgreSQL image and creates the
development database automatically. Wait for the container to report a healthy
status before applying migrations. Stopping the service preserves its data.
Avoid docker compose down -v unless you intentionally want to delete the local
development database.
pnpm db:migrate:devRuns Prisma migrations against the .env.dev database.
pnpm db:migrate:deployRuns Prisma migrations in deployment environments.
pnpm generateGenerates the Prisma client.
pnpm migrateCreates a new Prisma development migration using .env.dev. Prisma prompts for
the migration name. Use this command only after intentionally changing
prisma/schema.prisma; use pnpm db:migrate:dev to apply existing migrations.
pnpm buildCompiles TypeScript into dist.
pnpm startStarts the compiled API from dist/src/index.js.
pnpm testRuns the Jest test suite.
Prisma is the source of truth for the application schema. The production database is PostgreSQL hosted on Supabase.
The Prisma schema defines the following models:
UserSettingCollectionPost
Journally calls user-facing content “journal entries”; internally, journal
entries are represented by the Prisma Post model and /api/post endpoints.
Post.description is a Json field. It stores rich editor content in the JSON structure produced by Tiptap, preserving paragraphs, nodes, marks, and formatted text.
Database migrations live in:
prisma/migrationsThe local and deployment migration commands are documented in Available Scripts.
The project does not require or provide seed data. A fresh migration produces
an empty database; create the first account through POST /api/users/register,
Swagger UI, or the Journally Web registration screen.
Swagger UI is available when the server is running:
http://localhost:8080/swaggerIf you use a different PORT, update the URL accordingly.
Swagger configuration lives in:
src/swagger/swagger.tssrc/swagger/swaggerEntries.ts
Swagger documents reusable schemas, authentication headers, query parameters, request bodies, and the main API responses.
When calling POST /api/users/login from Swagger UI, the x-access-token and x-refresh-token headers returned by the API can be used to authorize protected endpoints.
The WebSocket endpoint listens for entry changes and autosaves editor content.
Local URL:
ws://localhost:8080/entriesProduction URL:
wss://your-api-domain.com/entriesSend the access token as the second WebSocket subprotocol so it is not exposed in URLs or proxy logs:
new WebSocket("wss://your-api-domain.com/entries", [
"access-token",
accessToken,
]);Autosave message:
{
"type": "entry:autosave",
"postId": 1,
"title": "Updated entry",
"description": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Content from Tiptap"
}
]
}
]
},
"clientRequestId": "optional-client-id"
}Successful response:
{
"type": "entry:saved",
"data": {
"status": 200,
"post": {}
},
"clientRequestId": "optional-client-id"
}Error response:
{
"type": "entry:error",
"error": true,
"data": "Error message"
}The socket verifies that the post belongs to the authenticated user before saving changes.
Journally API separates observability responsibilities to keep signals useful and avoid reporting the same event through every tool:
flowchart TD
A[Application] --> B[Expected error]
A --> C[Unexpected error]
A --> D[Strategic event]
B --> E[AppError or known middleware error]
E --> F[Safe HTTP 4xx]
C --> G[Sentry Issue]
C --> H[Winston]
H --> I[Vercel Runtime Logs]
C --> J[Generic HTTP 500]
D --> K[Sentry Logs]
Both Winston and Sentry attach the current environment using Vercel's
environment when available, then NODE_ENV, with development as the fallback.
Expected domain failures use AppError and return a safe HTTP 4xx response
without creating a Sentry Issue. Known JSON parser and body-processing failures
are classified through a closed mapping and follow the same safe 4xx path.
Unexpected exceptions reach the centralized Express error boundary. Sentry records the original exception as an Issue with its stack trace, Winston writes one structured error to the console for Vercel Runtime Logs, and the client receives a generic HTTP 500 response. Internal exception messages are not included in that response.
Winston provides the API's general technical and error logging independently of Sentry Logs. It writes through a console transport, which makes the output available in Vercel Runtime Logs. Development output is human-readable, production output is structured JSON, and logging is silent during tests.
Sentry Logs is reserved for low-frequency operational or security-relevant events which do not represent unexpected exceptions. It is not an access logger, a replacement for Winston, a mirror of console output, or a duplicate of Sentry Issues.
The currently instrumented events are:
user_session_revoked: logout removed a persisted refresh session.refresh_session_identity_mismatch: a valid refresh request referenced a persisted session owned by a different user identity.websocket_message_rate_limited: an authenticated WebSocket connection exceeded the autosave message limit.
These events use minimal structured metadata such as operation, status, reason
code, count, and transport when relevant. The Sentry SDK is configured with
sendDefaultPii: false. The instrumentation is designed not to add passwords,
JWTs, refresh tokens or their hashes, authorization headers, cookies, complete
request bodies, or journal-entry content to these events.
.
├── .github/workflows/ci-cd.yml
├── src
│ ├── app.ts
│ ├── controllers
│ │ ├── collectionsControllers.ts
│ │ ├── postControllers.ts
│ │ └── usersControllers.ts
│ ├── db/db.ts
│ ├── errors
│ │ ├── AppError.ts
│ │ └── middlewareErrors.ts
│ ├── loggers
│ │ ├── logger.ts
│ │ ├── observabilityLogger.ts
│ │ └── sentry.ts
│ ├── middlewares
│ │ ├── authenticatedToken.ts
│ │ ├── collectionValidation.ts
│ │ ├── errorHandler.ts
│ │ ├── notFound.ts
│ │ ├── postValidation.ts
│ │ ├── queryValidation.ts
│ │ └── userValidation.ts
│ ├── routes
│ │ ├── collections.ts
│ │ ├── post.ts
│ │ ├── routes.ts
│ │ └── users.ts
│ ├── services
│ │ ├── collectionServices.ts
│ │ ├── postServices.ts
│ │ └── usersServices.ts
│ ├── sockets/postSocket.ts
│ ├── swagger
│ │ ├── swagger.ts
│ │ └── swaggerEntries.ts
│ ├── utils/auth.ts
│ └── index.ts
├── tests
│ ├── helpers
│ │ ├── helperTest.ts
│ │ └── testDatabase.ts
│ ├── collection.test.ts
│ ├── collectionFindOneErrors.test.ts
│ ├── observability.test.ts
│ ├── post.test.ts
│ ├── postErrors.test.ts
│ ├── postSocket.test.ts
│ ├── user.test.ts
│ └── userErrors.test.ts
├── .env.test.example
├── compose.test.yaml
└── jest.config.tsThe project follows a layered structure:
HTTP -> Controller -> Service -> Prisma -> DatabaseControllers receive Express requests, read req.body, req.query, or req.params, check validation results, and delegate business logic to services.
Services contain the main business logic. They check entity ownership and existence, prepare data, run Prisma operations, and raise domain errors when expected business rules are not satisfied.
src/db/db.ts exports the Prisma client used by the service layer.
Middlewares handle JWT authentication, refresh token authentication, request validation, and not-found responses.
Swagger centralizes the OpenAPI documentation and serves the browser UI.
The REST integration suite uses Jest, ts-jest, and Supertest. Tests exercise
the Express application against PostgreSQL through Prisma and run with one Jest
worker to avoid interference in the shared test database.
Shared fixtures live in tests/helpers/helperTest.ts.
tests/helpers/testDatabase.ts protects cleanup operations by requiring
NODE_ENV=test and allowing only the journally_test and journally_ci
database names.
To run the suite locally:
cp .env.test.example .env.test
pnpm db:test:start
pnpm db:test:push
pnpm test
pnpm db:test:stopThe tests cover users, posts, and collections, including authentication, validation, ownership boundaries, pagination, post CRUD, collection CRUD, and collection assignment and deletion behavior. Observability tests also verify the classification of expected and unexpected errors, propagation of original exceptions, selective Sentry capture and application events, and the absence of duplicate reporting for expected failures.
CI provisions a PostgreSQL 17 service, applies committed Prisma migrations, and
runs the suite serially with pnpm test:ci. The WebSocket message-limit behavior
has focused automated coverage; full WebSocket autosave integration testing
remains a gap.
The diagram below was exported from Supabase after applying the Prisma migrations. It reflects the current PostgreSQL tables, including Prisma's internal _prisma_migrations table.
Notes:
_prisma_migrationsis managed by Prisma and should not be edited manually.Post.descriptionis stored as JSON to support Tiptap editor content.- The Prisma schema in
prisma/schema.prismaremains the source of truth for application models and migrations.
Before making this repository public:
- Keep
.env,.env.dev, and.env.prodignored and out of Git history. - Use only placeholders in
.env.example. - Rotate any JWT or database secrets that were shared outside the hosting provider.
- Store production secrets only in the hosting provider dashboard.
- Review screenshots and diagrams before committing them. Do not include connection strings, passwords, tokens, or internal project credentials.
