create-ej-app 17.0.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +89 -13
  2. package/cli.js +32 -19
  3. package/cli.ts +45 -27
  4. package/package.json +2 -2
  5. package/template/api/.env.example +3 -0
  6. package/template/api/.github/copilot-instructions.md +129 -0
  7. package/template/api/.vscode/extensions.json +9 -0
  8. package/template/api/.vscode/settings.json +3 -0
  9. package/template/api/Dockerfile +23 -0
  10. package/template/api/package.json +31 -0
  11. package/template/api/prettier.config.mjs +11 -0
  12. package/template/api/src/data-access/todo.ts +142 -0
  13. package/template/api/src/db/fields.ts +46 -0
  14. package/template/api/src/db/index.ts +21 -0
  15. package/template/api/src/db/schema.ts +19 -0
  16. package/template/api/src/env.ts +24 -0
  17. package/template/api/src/index.ts +39 -0
  18. package/template/api/src/lib/util.ts +55 -0
  19. package/template/api/src/routes/index.ts +18 -0
  20. package/template/api/src/routes/todo/index.ts +241 -0
  21. package/template/api/src/scripts/seed.ts +25 -0
  22. package/template/api/tsconfig.json +33 -0
  23. package/template/nextjs/drizzle.config.ts +10 -0
  24. /package/template/{base → api}/drizzle.config.ts +0 -0
  25. /package/template/{base → nextjs}/.dockerignore +0 -0
  26. /package/template/{base → nextjs}/.env.example +0 -0
  27. /package/template/{base → nextjs}/Dockerfile +0 -0
  28. /package/template/{base → nextjs}/components.json +0 -0
  29. /package/template/{base → nextjs}/eslint.config.mjs +0 -0
  30. /package/template/{base → nextjs}/next.config.ts +0 -0
  31. /package/template/{base → nextjs}/package.json +0 -0
  32. /package/template/{base → nextjs}/postcss.config.mjs +0 -0
  33. /package/template/{base → nextjs}/prettier.config.mjs +0 -0
  34. /package/template/{base → nextjs}/public/placeholder.svg +0 -0
  35. /package/template/{base → nextjs}/src/app/(admin)/admin/page.tsx +0 -0
  36. /package/template/{base → nextjs}/src/app/(admin)/layout.tsx +0 -0
  37. /package/template/{base → nextjs}/src/app/(app)/layout.tsx +0 -0
  38. /package/template/{base → nextjs}/src/app/(auth)/login/email-login-form.tsx +0 -0
  39. /package/template/{base → nextjs}/src/app/(auth)/login/page.tsx +0 -0
  40. /package/template/{base → nextjs}/src/app/(auth)/login/social-login-buttons.tsx +0 -0
  41. /package/template/{base → nextjs}/src/app/(marketing)/layout.tsx +0 -0
  42. /package/template/{base → nextjs}/src/app/(marketing)/page.tsx +0 -0
  43. /package/template/{base → nextjs}/src/app/(marketing)/privacy/page.tsx +0 -0
  44. /package/template/{base → nextjs}/src/app/(marketing)/terms/page.tsx +0 -0
  45. /package/template/{base → nextjs}/src/app/api/[[...slugs]]/route.ts +0 -0
  46. /package/template/{base → nextjs}/src/app/api/auth/[...all]/route.ts +0 -0
  47. /package/template/{base → nextjs}/src/app/favicon.ico +0 -0
  48. /package/template/{base → nextjs}/src/app/globals.css +0 -0
  49. /package/template/{base → nextjs}/src/app/layout.tsx +0 -0
  50. /package/template/{base → nextjs}/src/app/not-found.tsx +0 -0
  51. /package/template/{base → nextjs}/src/components/loader.tsx +0 -0
  52. /package/template/{base → nextjs}/src/components/react-query-provider.tsx +0 -0
  53. /package/template/{base → nextjs}/src/components/screen-size.tsx +0 -0
  54. /package/template/{base → nextjs}/src/components/site-footer.tsx +0 -0
  55. /package/template/{base → nextjs}/src/components/theme-toggle.tsx +0 -0
  56. /package/template/{base → nextjs}/src/components/ui/button.tsx +0 -0
  57. /package/template/{base → nextjs}/src/components/ui/card.tsx +0 -0
  58. /package/template/{base → nextjs}/src/components/ui/form.tsx +0 -0
  59. /package/template/{base → nextjs}/src/components/ui/input.tsx +0 -0
  60. /package/template/{base → nextjs}/src/components/ui/label.tsx +0 -0
  61. /package/template/{base → nextjs}/src/components/ui/separator.tsx +0 -0
  62. /package/template/{base → nextjs}/src/components/ui/sonner.tsx +0 -0
  63. /package/template/{base → nextjs}/src/db/index.ts +0 -0
  64. /package/template/{base → nextjs}/src/db/schema.ts +0 -0
  65. /package/template/{base → nextjs}/src/emails/components/email-layout.tsx +0 -0
  66. /package/template/{base → nextjs}/src/emails/magic-link.tsx +0 -0
  67. /package/template/{base → nextjs}/src/emails/test.tsx +0 -0
  68. /package/template/{base → nextjs}/src/emails/welcome.tsx +0 -0
  69. /package/template/{base → nextjs}/src/env.ts +0 -0
  70. /package/template/{base → nextjs}/src/instrumentation.ts +0 -0
  71. /package/template/{base → nextjs}/src/lib/auth-client.ts +0 -0
  72. /package/template/{base → nextjs}/src/lib/auth.ts +0 -0
  73. /package/template/{base → nextjs}/src/lib/cn.ts +0 -0
  74. /package/template/{base → nextjs}/src/lib/delay.ts +0 -0
  75. /package/template/{base → nextjs}/src/lib/eden.ts +0 -0
  76. /package/template/{base → nextjs}/src/lib/email-service.ts +0 -0
  77. /package/template/{base → nextjs}/src/lib/nodemailer.ts +0 -0
  78. /package/template/{base → nextjs}/src/lib/otel.ts +0 -0
  79. /package/template/{base → nextjs}/src/lib/safe-action-client.ts +0 -0
  80. /package/template/{base → nextjs}/src/lib/safe-action.ts +0 -0
  81. /package/template/{base → nextjs}/src/lib/siteConfig.ts +0 -0
  82. /package/template/{base → nextjs}/src/lib/toast-message.ts +0 -0
  83. /package/template/{base → nextjs}/src/scripts/test-mail.ts +0 -0
  84. /package/template/{base → nextjs}/terraform/artifact-registry.tf +0 -0
  85. /package/template/{base → nextjs}/terraform/backend-service.tf +0 -0
  86. /package/template/{base → nextjs}/terraform/cleanup.sh +0 -0
  87. /package/template/{base → nextjs}/terraform/cloudbuild.tf +0 -0
  88. /package/template/{base → nextjs}/terraform/cloudrun.tf +0 -0
  89. /package/template/{base → nextjs}/terraform/database.tf +0 -0
  90. /package/template/{base → nextjs}/terraform/forwarding-rule.tf +0 -0
  91. /package/template/{base → nextjs}/terraform/ip-address.tf +0 -0
  92. /package/template/{base → nextjs}/terraform/network-endpoint-group.tf +0 -0
  93. /package/template/{base → nextjs}/terraform/network.tf +0 -0
  94. /package/template/{base → nextjs}/terraform/pre.sh +0 -0
  95. /package/template/{base → nextjs}/terraform/providers.tf +0 -0
  96. /package/template/{base → nextjs}/terraform/secret.txt +0 -0
  97. /package/template/{base → nextjs}/terraform/setup.sh +0 -0
  98. /package/template/{base → nextjs}/terraform/ssl-certs.tf +0 -0
  99. /package/template/{base → nextjs}/terraform/target-https-proxy copy.tf +0 -0
  100. /package/template/{base → nextjs}/terraform/target-https-proxy.tf +0 -0
  101. /package/template/{base → nextjs}/terraform/terraform.tfvars +0 -0
  102. /package/template/{base → nextjs}/terraform/url-map.tf +0 -0
  103. /package/template/{base → nextjs}/terraform/variables.tf +0 -0
  104. /package/template/{base → nextjs}/tsconfig.json +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # create-ej-app
2
2
 
3
- A no-bullshit CLI to bootstrap a production-ready **fucking beast** of a app with Next.js, Better Auth, Drizzle ORM, Elysia, Workflow, shadcn/ui, React Email, OTEL, and deployment wired up so you don’t screw your shit up in production.
3
+ A no-bullshit CLI to bootstrap production-ready **fucking beasts** of apps with Next.js, Better Auth, Drizzle ORM, Elysia, Workflow, shadcn/ui, React Email, OTEL, and deployment wired up so you don’t screw your shit up in production.
4
4
 
5
5
  ```bash
6
6
  bunx create-ej-app@latest
@@ -49,6 +49,20 @@ bunx create-ej-app@latest
49
49
 
50
50
  ---
51
51
 
52
+ ## 📋 Templates (Pick your poison)
53
+
54
+ Choose your template based on what you’re building:
55
+
56
+ ### Next.js Full-Stack App
57
+
58
+ Full-stack setup with Next.js 16, Better Auth, shadcn/ui, and Drizzle ORM. Perfect for web apps that need auth, UI, and backend APIs.
59
+
60
+ ### API Server
61
+
62
+ Production-ready REST API built with Elysia on Bun, Drizzle ORM, PostgreSQL, and Zod validation. Perfect for headless APIs, microservices, internal tools, or that backend you actually want to scale without it exploding like fragile shit.
63
+
64
+ ---
65
+
52
66
  ## 🚀 Quick Start (Let’s fucking go)
53
67
 
54
68
  ```bash
@@ -59,7 +73,7 @@ You’ll be prompted for:
59
73
 
60
74
  - Project name
61
75
  - Description
62
- - Template
76
+ - Template (nextjs or api)
63
77
  - Git init (because not using git in 2026 is wild)
64
78
 
65
79
  Example:
@@ -67,7 +81,7 @@ Example:
67
81
  ```bash
68
82
  ? Enter the project name: badass-app
69
83
  ? Enter a description: My savage Next.js monster
70
- ? Select a template: base
84
+ ? Select a template: nextjs
71
85
  ? Initialize a git repository? yes
72
86
  ```
73
87
 
@@ -77,7 +91,9 @@ Boom. Done.
77
91
 
78
92
  ## 📦 What You Actually Get
79
93
 
80
- ### Tech Stack (The real weapons)
94
+ ### Next.js Full-Stack Template
95
+
96
+ **Tech Stack (The real weapons)**
81
97
 
82
98
  - **Framework**: Next.js
83
99
  - **Runtime**: Bun (fast as shit)
@@ -93,6 +109,20 @@ Boom. Done.
93
109
  - **Forms**: react-hook-form + Zod
94
110
  - **Theme**: Dark mode out of the box
95
111
 
112
+ ### API Server Template
113
+
114
+ **Tech Stack (The real weapons)**
115
+
116
+ - **Runtime**: Bun (blazing fast as fuck)
117
+ - **Framework**: Elysia
118
+ - **Database**: PostgreSQL + Drizzle ORM
119
+ - **Validation**: Zod v4
120
+ - **API Docs**: Auto-generated OpenAPI docs
121
+ - **Containerisation**: Docker with distroless images
122
+ - **Type Checking**: TypeScript with strict mode
123
+ - **Formatting**: Prettier
124
+ - **Environment**: Validated env vars with @t3-oss/env-core
125
+
96
126
  ---
97
127
 
98
128
  ## 🛠 Setup & Development
@@ -101,8 +131,8 @@ Boom. Done.
101
131
 
102
132
  - Node 24+ or Bun
103
133
  - PostgreSQL running
104
- - OAuth creds if you want GitHub/Google login
105
- - Email provider (Resend, SES, Postmark, etc.)
134
+ - OAuth creds if you want GitHub/Google login (Next.js template)
135
+ - Email provider (Resend, SES, Postmark, etc.) (Next.js template)
106
136
 
107
137
  ### Install
108
138
 
@@ -124,26 +154,38 @@ Don’t commit this crap. Seriously.
124
154
 
125
155
  ## 💻 Development
126
156
 
157
+ ### Next.js Template
158
+
127
159
  ```bash
128
160
  bun run dev
129
161
  ```
130
162
 
131
163
  Open `http://localhost:3000` and admire your badass setup.
132
164
 
165
+ ### API Template
166
+
167
+ ```bash
168
+ bun run dev
169
+ ```
170
+
171
+ API runs on `http://localhost:3000`. OpenAPI docs at `http://localhost:3000/docs`.
172
+
173
+ Fire it up, hit the endpoints, break things on purpose, and watch it handle your shit properly.
174
+
133
175
  ---
134
176
 
135
177
  ## 🐳 Docker (Ship it like a pro)
136
178
 
137
179
  ```bash
138
- docker build -t my-app .
139
- docker run -p 3000:3000 my-app
180
+ bun run build:docker
181
+ docker run -p 3000:3000 my-awesome-project
140
182
  ```
141
183
 
142
- No “works on my machine” bullshit anymore.
184
+ No "works on my machine" bullshit anymore.
143
185
 
144
186
  ---
145
187
 
146
- ## ☁️ Infrastructure (The Cloud Shit)
188
+ ## ☁️ Infrastructure (The Cloud Shit) - Next.js Template
147
189
 
148
190
  Terraform config included for:
149
191
 
@@ -167,7 +209,7 @@ And now you’re running production infra like a grown-ass engineer.
167
209
 
168
210
  ---
169
211
 
170
- ## 🔐 Authentication
212
+ ## 🔐 Authentication - Next.js Template
171
213
 
172
214
  Better Auth gives you:
173
215
 
@@ -181,7 +223,7 @@ Secure, extensible, no duct-taped auth spaghetti.
181
223
 
182
224
  ---
183
225
 
184
- ## 🎨 UI Components
226
+ ## 🎨 UI Components - Next.js Template
185
227
 
186
228
  Preinstalled:
187
229
 
@@ -201,7 +243,7 @@ Ship beautiful shit without reinventing buttons for the 900th time.
201
243
 
202
244
  ---
203
245
 
204
- ## 🎯 Route Groups
246
+ ## 🎯 Route Groups - Next.js Template
205
247
 
206
248
  Organized like a sane human:
207
249
 
@@ -216,6 +258,8 @@ No messy folder hell.
216
258
 
217
259
  ## 🔧 Customization
218
260
 
261
+ ### Next.js Template
262
+
219
263
  Edit:
220
264
 
221
265
  - `siteConfig.ts` – change name/description
@@ -224,6 +268,38 @@ Edit:
224
268
 
225
269
  Then push schema and move on with your life.
226
270
 
271
+ ### API Template
272
+
273
+ Edit:
274
+
275
+ - `src/db/schema.ts` – change DB schema and model your data like a boss
276
+ - `src/routes/` – add new API endpoints and wire your logic properly
277
+ - `src/env.ts` – add new env vars and validate that shit
278
+
279
+ Push schema with `bun run db:push` and type-check with `bun run type-check`.
280
+
281
+ ---
282
+
283
+ ## 🏗️ API Template Architecture
284
+
285
+ Built with a clean layered architecture (no spaghetti garbage):
286
+
287
+ - **Routes** (`src/routes/`) – HTTP endpoints, validation, call data-access. Keep it sharp.
288
+ - **Data-Access** (`src/data-access/`) – Direct DB interactions per resource. No random queries scattered like WTF.
289
+ - **Database** (`src/db/`) – Drizzle client, schemas, reusable fields. Your source of truth.
290
+ - **Library** (`src/lib/`) – Shared utilities and Zod schemas so you don’t duplicate crap everywhere.
291
+
292
+ Includes a full CRUD API for todos with pagination, sorting, search, and OpenAPI docs so you can see your shit documented without extra effort.
293
+
294
+ ### API Commands
295
+
296
+ - `bun run dev` – Start dev server with hot-reload
297
+ - `bun run build` – Type-check and compile to native binary
298
+ - `bun run type-check` – Run TypeScript checker
299
+ - `bun run db:push` – Push schema to DB
300
+ - `bun run db:studio` – Open Drizzle Studio
301
+ - `bun run db:seed` – Seed database
302
+
227
303
  ---
228
304
 
229
305
  ## 🤝 Contributing
package/cli.js CHANGED
@@ -62,13 +62,13 @@ commander_1.program.action(() => __awaiter(void 0, void 0, void 0, function* ()
62
62
  type: 'input',
63
63
  name: 'projectName',
64
64
  message: 'Enter the project name:',
65
- initial: 'my-app'
65
+ initial: 'my-project'
66
66
  },
67
67
  {
68
68
  type: 'input',
69
69
  name: 'description',
70
70
  message: 'Enter a description for the project:',
71
- initial: 'A Legendary Next.js App'
71
+ initial: 'A Legendary Project'
72
72
  },
73
73
  {
74
74
  type: 'select',
@@ -76,8 +76,12 @@ commander_1.program.action(() => __awaiter(void 0, void 0, void 0, function* ()
76
76
  message: 'Select a template:',
77
77
  choices: [
78
78
  {
79
- name: 'base',
80
- message: 'Base setup with Next 16, Better Auth, shadcn and Drizzle'
79
+ name: 'nextjs',
80
+ message: 'Full Stack setup with Next 16, Better Auth, shadcn and Drizzle'
81
+ },
82
+ {
83
+ name: 'api',
84
+ message: 'API Server setup with Elysia, Drizzle, Typescript'
81
85
  }
82
86
  ]
83
87
  },
@@ -102,27 +106,36 @@ commander_1.program.action(() => __awaiter(void 0, void 0, void 0, function* ()
102
106
  packageJson.name = projectName;
103
107
  packageJson.description = description;
104
108
  fs_1.default.writeFileSync(packageJsonFile, JSON.stringify(packageJson, null, 2));
105
- const siteConfigFile = path_1.default.join(projectDir, 'src', 'lib', 'siteConfig.ts');
106
- let siteConfig = fs_1.default.readFileSync(siteConfigFile, 'utf-8');
107
- siteConfig = siteConfig.replace('Project Name', projectName);
108
- siteConfig = siteConfig.replace('Project Description', description);
109
- fs_1.default.writeFileSync(siteConfigFile, siteConfig);
110
109
  const envExampleFile = path_1.default.join(projectDir, '.env.example');
111
110
  const envExample = fs_1.default.readFileSync(envExampleFile, 'utf-8');
112
111
  fs_1.default.writeFileSync(path_1.default.join(projectDir, '.env'), envExample);
113
- fs_1.default.writeFileSync(path_1.default.join(projectDir, '.gitignore'), `node_modules
114
- .next
115
- out
116
- build
117
- next-env.d.ts
112
+ if (template === 'nextjs') {
113
+ const siteConfigFile = path_1.default.join(projectDir, 'src', 'lib', 'siteConfig.ts');
114
+ let siteConfig = fs_1.default.readFileSync(siteConfigFile, 'utf-8');
115
+ siteConfig = siteConfig.replace('Project Name', projectName);
116
+ siteConfig = siteConfig.replace('Project Description', description);
117
+ fs_1.default.writeFileSync(siteConfigFile, siteConfig);
118
+ fs_1.default.writeFileSync(path_1.default.join(projectDir, '.gitignore'), `node_modules
119
+ .next
120
+ out
121
+ build
122
+ next-env.d.ts
123
+ .DS_Store
124
+ .env
125
+ `);
126
+ fs_1.default.writeFileSync(path_1.default.join(projectDir, 'terraform', '.gitignore'), `.terraform
127
+ *.tfstate
128
+ *.tfstate.*
129
+ .terraform.lock.hcl
130
+ `);
131
+ }
132
+ if (template === 'api') {
133
+ fs_1.default.writeFileSync(path_1.default.join(projectDir, '.gitignore'), `node_modules
118
134
  .DS_Store
119
135
  .env
136
+ server
120
137
  `);
121
- fs_1.default.writeFileSync(path_1.default.join(projectDir, 'terraform', '.gitignore'), `.terraform
122
- *.tfstate
123
- *.tfstate.*
124
- .terraform.lock.hcl
125
- `);
138
+ }
126
139
  fs_1.default.writeFileSync(path_1.default.join(projectDir, 'README.md'), `# ${projectName}
127
140
 
128
141
  ${description}
package/cli.ts CHANGED
@@ -59,13 +59,13 @@ program.action(async () => {
59
59
  type: 'input',
60
60
  name: 'projectName',
61
61
  message: 'Enter the project name:',
62
- initial: 'my-app'
62
+ initial: 'my-project'
63
63
  },
64
64
  {
65
65
  type: 'input',
66
66
  name: 'description',
67
67
  message: 'Enter a description for the project:',
68
- initial: 'A Legendary Next.js App'
68
+ initial: 'A Legendary Project'
69
69
  },
70
70
  {
71
71
  type: 'select',
@@ -73,8 +73,13 @@ program.action(async () => {
73
73
  message: 'Select a template:',
74
74
  choices: [
75
75
  {
76
- name: 'base',
77
- message: 'Base setup with Next 16, Better Auth, shadcn and Drizzle'
76
+ name: 'nextjs',
77
+ message:
78
+ 'Full Stack setup with Next 16, Better Auth, shadcn and Drizzle'
79
+ },
80
+ {
81
+ name: 'api',
82
+ message: 'API Server setup with Elysia, Drizzle, Typescript'
78
83
  }
79
84
  ]
80
85
  },
@@ -106,36 +111,49 @@ program.action(async () => {
106
111
  packageJson.description = description
107
112
  fs.writeFileSync(packageJsonFile, JSON.stringify(packageJson, null, 2))
108
113
 
109
- const siteConfigFile = path.join(projectDir, 'src', 'lib', 'siteConfig.ts')
110
- let siteConfig = fs.readFileSync(siteConfigFile, 'utf-8')
111
- siteConfig = siteConfig.replace('Project Name', projectName)
112
- siteConfig = siteConfig.replace('Project Description', description)
113
- fs.writeFileSync(siteConfigFile, siteConfig)
114
-
115
114
  const envExampleFile = path.join(projectDir, '.env.example')
116
115
  const envExample = fs.readFileSync(envExampleFile, 'utf-8')
117
116
  fs.writeFileSync(path.join(projectDir, '.env'), envExample)
118
117
 
119
- fs.writeFileSync(
120
- path.join(projectDir, '.gitignore'),
121
- `node_modules
122
- .next
123
- out
124
- build
125
- next-env.d.ts
126
- .DS_Store
127
- .env
118
+ if (template === 'nextjs') {
119
+ const siteConfigFile = path.join(projectDir, 'src', 'lib', 'siteConfig.ts')
120
+ let siteConfig = fs.readFileSync(siteConfigFile, 'utf-8')
121
+ siteConfig = siteConfig.replace('Project Name', projectName)
122
+ siteConfig = siteConfig.replace('Project Description', description)
123
+ fs.writeFileSync(siteConfigFile, siteConfig)
124
+
125
+ fs.writeFileSync(
126
+ path.join(projectDir, '.gitignore'),
127
+ `node_modules
128
+ .next
129
+ out
130
+ build
131
+ next-env.d.ts
132
+ .DS_Store
133
+ .env
128
134
  `
129
- )
135
+ )
136
+
137
+ fs.writeFileSync(
138
+ path.join(projectDir, 'terraform', '.gitignore'),
139
+ `.terraform
140
+ *.tfstate
141
+ *.tfstate.*
142
+ .terraform.lock.hcl
143
+ `
144
+ )
145
+ }
130
146
 
131
- fs.writeFileSync(
132
- path.join(projectDir, 'terraform', '.gitignore'),
133
- `.terraform
134
- *.tfstate
135
- *.tfstate.*
136
- .terraform.lock.hcl
147
+ if (template === 'api') {
148
+ fs.writeFileSync(
149
+ path.join(projectDir, '.gitignore'),
150
+ `node_modules
151
+ .DS_Store
152
+ .env
153
+ server
137
154
  `
138
- )
155
+ )
156
+ }
139
157
 
140
158
  fs.writeFileSync(
141
159
  path.join(projectDir, 'README.md'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-ej-app",
3
- "version": "17.0.0",
3
+ "version": "18.0.0",
4
4
  "description": "Bootstrap a next.js app with my preferred setup",
5
5
  "main": "cli.js",
6
6
  "scripts": {
@@ -35,7 +35,7 @@
35
35
  "enquirer": "^2.4.1"
36
36
  },
37
37
  "devDependencies": {
38
- "@types/node": "^25.2.3",
38
+ "@types/node": "^25.3.0",
39
39
  "typescript": "^5.9.3"
40
40
  }
41
41
  }
@@ -0,0 +1,3 @@
1
+ PORT="3000"
2
+ APP_ENV="production"
3
+ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
@@ -0,0 +1,129 @@
1
+ # Copilot Instructions
2
+
3
+ ## Project Overview
4
+
5
+ This project is a production-ready REST API built with **Elysia** on the **Bun** runtime. It uses **Drizzle ORM** with **PostgreSQL**, **Zod v4** for validation, and ships with auto-generated OpenAPI docs.
6
+
7
+ ## Tech Stack
8
+
9
+ - **Runtime:** Bun (not Node.js) — use `bun` for all CLI commands
10
+ - **HTTP Framework:** Elysia
11
+ - **Database:** PostgreSQL via Drizzle ORM (using the `bun-sql` driver)
12
+ - **Validation:** Zod v4
13
+ - **Environment Validation:** @t3-oss/env-core + Zod
14
+ - **API Docs:** @elysiajs/openapi (served at `/docs`)
15
+ - **Type Checker:** @typescript/native-preview (`tsgo`) — NOT `tsc`
16
+ - **Formatter:** Prettier (with `prettier-plugin-organize-imports`)
17
+ - **Containerisation:** Docker with multi-stage build producing a distroless image
18
+
19
+ ## Project Structure
20
+
21
+ ```
22
+ src/
23
+ ├── index.ts # App entry point – boots the Elysia server
24
+ ├── env.ts # Validated env vars (Zod + t3-env)
25
+ ├── data-access/ # Database query functions (one file per resource)
26
+ │ └── todo.ts
27
+ ├── db/
28
+ │ ├── fields.ts # Reusable Drizzle column definitions
29
+ │ ├── index.ts # Drizzle client singleton
30
+ │ └── schema.ts # All Drizzle table definitions
31
+ ├── lib/
32
+ │ └── util.ts # Shared Zod schemas (pagination, sorting)
33
+ └── routes/
34
+ ├── index.ts # Top-level /api router
35
+ └── todo/
36
+ └── index.ts # CRUD routes for /api/todos
37
+ ```
38
+
39
+ ## Architecture & Conventions
40
+
41
+ ### Layered Architecture
42
+
43
+ The project follows a strict separation of concerns:
44
+
45
+ 1. **Routes** (`src/routes/`) — Define HTTP endpoints, request/response validation, and call data-access functions. Route handlers never import `db` directly.
46
+ 2. **Data-Access** (`src/data-access/`) — All direct database interactions. One file per resource. Route handlers call these functions exclusively.
47
+ 3. **Database** (`src/db/`) — Drizzle client singleton, table schema definitions, and reusable column helpers.
48
+ 4. **Library** (`src/lib/`) — Shared utilities, reusable Zod schema fragments.
49
+
50
+ ### Adding a New Resource
51
+
52
+ 1. Create the Drizzle table in `src/db/schema.ts` (use `commonFieldDefs` from `src/db/fields.ts` for id, `createdAt`, `updatedAt`).
53
+ 2. Create `src/data-access/<resource>.ts` with all DB query functions.
54
+ 3. Create `src/routes/<resource>/index.ts` with the Elysia router.
55
+ 4. Register the new router in `src/routes/index.ts` via `.use()`.
56
+ 5. Push Schema changes with `bun run db:push`.
57
+ 6. Run `bun type-check` to ensure types are correct across layers.
58
+
59
+ ### Route Conventions
60
+
61
+ - All API routes live under the `/api` prefix.
62
+ - Each resource router uses `new Elysia({ prefix: '/<resource>' })`.
63
+ - Every route handler validates input using Zod schemas in the third argument to route methods (`query`, `params`, `body`).
64
+ - UUID path params are validated with `z.uuid()`.
65
+ - Each route includes a `detail` object with `description` and `tags` for OpenAPI documentation.
66
+ - Handlers use try/catch, returning `{ success: true, data: { ... } }` on success and `status(statusCode, { success: false, error: '...' })` on failure.
67
+ - Use `status()` from `elysia` (not `.set.status`) for error responses.
68
+
69
+ ### Database & Schema Conventions
70
+
71
+ - All tables use **UUID** primary keys generated by the database (`defaultRandom()`).
72
+ - All tables include `created_at` and `updated_at` timestamp columns via the `commonFieldDefs.dates` spread.
73
+ - Use `commonFieldDefs.id` for the primary key column.
74
+ - The Drizzle client is a singleton exported from `src/db/index.ts`.
75
+ - Column names in the database use **snake_case** (`created_at`), while TypeScript properties use **camelCase** (`createdAt`).
76
+ - Schema definitions are in `src/db/schema.ts` — the single source of truth shared by both the app and Drizzle Kit.
77
+
78
+ ### Validation & Environment
79
+
80
+ - Zod v4 is used for all request validation (NOT Elysia's built-in typebox schemas).
81
+ - Environment variables are validated at startup via `src/env.ts`. If a variable is missing or invalid the app refuses to start.
82
+ - Import env as `import { env } from '~/env'`.
83
+
84
+ ### Path Aliases
85
+
86
+ - `~/*` maps to `./src/*` (configured in `tsconfig.json`). Always use `~/` imports for project files.
87
+
88
+ ### Code Style
89
+
90
+ - **No semicolons** — the project uses Prettier with `semi: false`.
91
+ - **Single quotes** for strings.
92
+ - **No trailing commas** (`trailingComma: 'none'`).
93
+ - **Arrow parens:** avoid when possible (`arrowParens: 'avoid'`).
94
+ - **Tab width:** 2 spaces.
95
+ - Imports are auto-organised by `prettier-plugin-organize-imports`.
96
+ - Use `const` arrow functions for exported functions (not `function` declarations).
97
+ - Add JSDoc comments on all exports describing their purpose.
98
+
99
+ ### TypeScript
100
+
101
+ - Strict mode is enabled with all strict checks (`noImplicitAny`, `strictNullChecks`, `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noUncheckedIndexedAccess`, etc.).
102
+ - Target is `ESNext` with `module: ESNext` and `moduleResolution: bundler`.
103
+ - Type checking is done with `tsgo` (`bun run type-check`), not `tsc`.
104
+
105
+ ### Pagination & Sorting
106
+
107
+ - Use the `paginationSchema` spread from `src/lib/util.ts` for paginated list endpoints (offset-based, 1-indexed pages, default limit 10, max 100).
108
+ - Use `getSortSchema(['field1', 'field2', ...])` for sortable endpoints.
109
+ - List responses follow the shape: `{ success, data: { page, limit, hasMore, <items>, count } }`.
110
+
111
+ ### Error Handling
112
+
113
+ - Route handlers wrap logic in try/catch.
114
+ - Log errors with `console.error('Error in METHOD /path', err)`.
115
+ - Return `status(500, { success: false, error: 'Something went wrong.' })` for unexpected errors.
116
+ - Return `status(404, { success: false, error: '<Resource> not found' })` for missing resources.
117
+
118
+ ## Commands
119
+
120
+ | Command | Purpose |
121
+ | ---------------------- | ------------------------------------- |
122
+ | `bun dev` | Start dev server with hot-reload |
123
+ | `bun run build` | Type-check + compile to native binary |
124
+ | `bun run type-check` | Run tsgo type checker |
125
+ | `bun run format` | Format code with Prettier |
126
+ | `bun run db:push` | Push Drizzle schema to the database |
127
+ | `bun run db:studio` | Open Drizzle Studio |
128
+ | `bun run db:seed` | Run seed script |
129
+ | `bun run build:docker` | Build Docker image |
@@ -0,0 +1,9 @@
1
+ {
2
+ "recommendations": [
3
+ "eamodio.gitlens",
4
+ "esbenp.prettier-vscode",
5
+ "TypeScriptTeam.native-preview",
6
+ "usernamehw.errorlens",
7
+ "YoavBls.pretty-ts-errors"
8
+ ]
9
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "typescript.experimental.useTsgo": true
3
+ }
@@ -0,0 +1,23 @@
1
+ FROM oven/bun:latest AS build
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package.json bun.lock ./
6
+
7
+ RUN bun install --frozen-lockfile
8
+
9
+ COPY . .
10
+
11
+ ENV NODE_ENV=production
12
+
13
+ RUN bun run build
14
+
15
+ FROM gcr.io/distroless/base
16
+
17
+ WORKDIR /app
18
+
19
+ COPY --from=build /app/server server
20
+
21
+ ENV NODE_ENV=production
22
+
23
+ ENTRYPOINT ["./server"]
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "my-api",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "main": "src/index.ts",
6
+ "scripts": {
7
+ "dev": "bun --watch .",
8
+ "build": "bun type-check && bun build --compile --target bun --outfile server .",
9
+ "build:docker": "docker build -t my-api .",
10
+ "type-check": "tsgo --incremental false",
11
+ "format": "prettier --write \"./src/**/*.{js,cjs,mjs,jsx,ts,tsx}\"",
12
+ "db:push": "drizzle-kit push",
13
+ "db:studio": "drizzle-kit studio",
14
+ "db:seed": "bun ./src/scripts/seed.ts"
15
+ },
16
+ "dependencies": {
17
+ "@elysiajs/openapi": "1.4.14",
18
+ "@t3-oss/env-core": "0.13.10",
19
+ "drizzle-orm": "0.45.1",
20
+ "elysia": "1.4.25",
21
+ "zod": "4.3.6"
22
+ },
23
+ "devDependencies": {
24
+ "@types/bun": "1.3.9",
25
+ "@typescript/native-preview": "7.0.0-dev.20260217.1",
26
+ "drizzle-kit": "0.31.9",
27
+ "postgres": "3.4.8",
28
+ "prettier": "3.8.1",
29
+ "prettier-plugin-organize-imports": "4.3.0"
30
+ }
31
+ }
@@ -0,0 +1,11 @@
1
+ /** @type {import('prettier').Config} */
2
+ const config = {
3
+ plugins: ['prettier-plugin-organize-imports'],
4
+ arrowParens: 'avoid',
5
+ trailingComma: 'none',
6
+ semi: false,
7
+ singleQuote: true,
8
+ tabWidth: 2
9
+ }
10
+
11
+ export default config