Demo video: https://www.youtube.com/watch?v=OohYyQAmHrU
This repo includes a Backend (Bun + Elysia) that queries data from a Notion Data Source and a Frontend (Next.js) that renders a Sales CRM table with Sort/Filter.
- Sales CRM Table: displays sales data (fetched from a Notion Data Source)
- Sort: sends sort criteria to the backend (multi-sort)
- Compound Filter:
and/orfilter tree (with max nesting depth) - API Docs:
- Scalar: UI + OpenAPI JSON
- Swagger UI: UI + OpenAPI JSON
- Tests + Coverage (Backend): Jest + HTML report (
coverage/lcov-report/index.html) - Docker: run full stack with
docker compose
backend/: Bun + Elysia API, queries Notion, validates + applies filtersfrontend-application/: Next.js app (Sales CRM UI)docker-compose.yml: run FE + BE togetherenv.example: sample env vars (for docker compose)
- Backend: install Bun (recommended)
- Frontend: Node.js 18+ and npm
- Docker (optional): Docker Desktop + Docker Compose v2
- Required credentials:
NOTION_TOKENNOTION_DATA_SOURCE_ID
If you already have Node.js installed, you can install Bun globally via npm:
npm i -g bun
bun --versioncd backend
bun installBackend reads these environment variables:
PORT(default3001)ALLOWED_URLS(CORS origins, CSV format, e.g.http://localhost:3000)NOTION_TOKEN(required)NOTION_DATA_SOURCE_ID(required)MAX_FILTER_DEPTH(default2)
Example (bash):
export PORT=3001
export ALLOWED_URLS=http://localhost:3000
export NOTION_TOKEN=...
export NOTION_DATA_SOURCE_ID=...
export MAX_FILTER_DEPTH=2Windows PowerShell (equivalent):
$env:PORT="3001"
$env:ALLOWED_URLS="http://localhost:3000"
$env:NOTION_TOKEN="..."
$env:NOTION_DATA_SOURCE_ID="..."
$env:MAX_FILTER_DEPTH="2"bun run devBackend runs at:
- API base:
http://localhost:3001/api - Health:
GET http://localhost:3001/api/→{ message: "BE API is running" }
When the backend is running:
- UI:
http://localhost:3001/api/scalar - OpenAPI JSON:
http://localhost:3001/api/scalar/json
- UI:
http://localhost:3001/api/swagger - OpenAPI JSON:
http://localhost:3001/api/swagger/json
cd frontend-application
npm ciFrontend calls the backend via:
NEXT_PUBLIC_BACKEND_API_URL(expected defaulthttp://localhost:3001)- (Optional)
NEXT_PUBLIC_MAX_FILTER_DEPTH(default2, filter nesting limit in the UI)
Example (bash):
export NEXT_PUBLIC_BACKEND_API_URL=http://localhost:3001
export NEXT_PUBLIC_MAX_FILTER_DEPTH=2Windows PowerShell:
$env:NEXT_PUBLIC_BACKEND_API_URL="http://localhost:3001"
$env:NEXT_PUBLIC_MAX_FILTER_DEPTH="2"npm run devOpen the UI:
http://localhost:3000
Frontend calls:
POST /api/sales
Example payload:
{
"sorts": [
{ "property": "Estimated_value", "direction": "ascending" }
],
"filter": {
"and": [
{ "property": "status", "filterOperator": "is", "value": "Won" },
{ "or": [
{ "property": "accountOwner", "filterOperator": "contains", "value": "john" },
{ "property": "accountOwner", "filterOperator": "contains", "value": "anna" }
]}
]
}
}Notes:
sorts[].property: FE sends column display names with spaces replaced by_(e.g."Estimated value"→"Estimated_value"). Backend converts_back to" "to query Notion.filter.property: uses camelCase keys (backend parses Notion column names:"Estimated value"→estimatedValue)- Filters support
and/orgroups and conditions viafilterOperator(string/number/date/checkbox/multi_select…) - If filters are nested too deeply, it will error based on
MAX_FILTER_DEPTH
cd backend
bun run testcd backend
bun run test-coverageAfter running, it generates:
backend/coverage/- HTML report:
backend/coverage/lcov-report/index.html
Common approach (VS Code):
- Open
backend/coverage/lcov-report/index.html - Right click → Open with Live Server
Or use any static server (example):
cd backend/coverage/lcov-report
npx --yes serve .Copy env.example → .env (at the repo root) and fill:
NOTION_TOKENNOTION_DATA_SOURCE_ID
Note: In real projects,
.envfiles should be added to.gitignoreto avoid accidentally committing secrets. For this assignment, I’m intentionally documenting the.envsetup here to make the app easier to test end-to-end. My Notion integration is configured with read-only access, so it’s lower risk for demonstrating/testing purposes.
Important .env vars:
FRONTEND_PORT(default3000)BACKEND_PORT(default3001)NEXT_PUBLIC_BACKEND_API_URL(defaulthttp://localhost:3001)ALLOWED_URLS(defaulthttp://localhost:3000)MAX_FILTER_DEPTH(default2)
docker compose build
docker compose up- Frontend:
http://localhost:3000 - Backend:
http://localhost:3001/api - Scalar:
http://localhost:3001/api/scalar - Swagger:
http://localhost:3001/api/swagger
- CORS error: verify
ALLOWED_URLSincludes your FE origin (e.g.http://localhost:3000) and is in CSV format. - FE cannot reach BE: verify
NEXT_PUBLIC_BACKEND_API_URL(local/dev or docker) points to the correct host/port. - Notion auth/data source errors: verify
NOTION_TOKENandNOTION_DATA_SOURCE_ID.
👤 Phan Quang Minh Long (Mike) 💼 MIS | IT | SWE | FE | Dev 🧑💻 Frontend Engineer
📱 Phone: Zalo (+84) 0852197589 📧 Email: mailto:phanquangminhlong@gmail.com 📍 Location: Ho Chi Minh City
