neon-prisma-starter 1.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.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # neon-prisma-starter
2
+
3
+ > Instantly scaffold a Node.js + Prisma + Neon (serverless Postgres) project with one command.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx neon-prisma-starter my-project
9
+ ```
10
+
11
+ Or install globally:
12
+
13
+ ```bash
14
+ npm install -g neon-prisma-starter
15
+ neon-prisma-starter my-project
16
+ ```
17
+
18
+ ## What gets created
19
+
20
+ ```
21
+ my-project/
22
+ ├── prisma/
23
+ │ ├── schema.prisma ← Author + Book models ready to go
24
+ │ └── seed.js ← Example seed data
25
+ ├── src/
26
+ │ ├── generated/prisma/ ← auto-generated (do not touch)
27
+ │ ├── db.js ← Prisma client with Neon adapter
28
+ │ └── index.js ← Express server with CRUD routes
29
+ ├── .env ← your DATABASE_URL goes here
30
+ ├── .gitignore
31
+ └── package.json
32
+ ```
33
+
34
+ ## After scaffolding
35
+
36
+ ```bash
37
+ cd my-project
38
+
39
+ # 1. Add your Neon DATABASE_URL to .env (if not provided during setup)
40
+
41
+ # 2. Create tables in Neon
42
+ npm run migrate
43
+
44
+ # 3. Seed example data
45
+ npm run seed
46
+
47
+ # 4. Start the server
48
+ npm run dev
49
+ ```
50
+
51
+ ## API Endpoints
52
+
53
+ | Method | Route | Description |
54
+ |--------|----------------|--------------------------|
55
+ | GET | / | Health check |
56
+ | GET | /authors | List all authors + books |
57
+ | POST | /authors | Create a new author |
58
+ | GET | /authors/:id | Get author by ID |
59
+ | DELETE | /authors/:id | Delete author |
60
+ | GET | /books | List all books |
61
+ | POST | /books | Create a new book |
62
+ | DELETE | /books/:id | Delete a book |
63
+
64
+ ## Scripts
65
+
66
+ | Command | Description |
67
+ |-------------------|--------------------------------------|
68
+ | `npm run dev` | Start server with nodemon |
69
+ | `npm run migrate` | Run Prisma migrations on Neon |
70
+ | `npm run seed` | Seed database with example data |
71
+ | `npm run studio` | Open Prisma Studio (visual DB UI) |
72
+
73
+ ## Requirements
74
+
75
+ - Node.js 18+
76
+ - A [Neon](https://neon.tech) account (free tier works)
77
+
78
+ ## License
79
+
80
+ MIT
package/bin/index.js ADDED
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const fs = require('fs-extra');
6
+ const path = require('path');
7
+ const { execSync } = require('child_process');
8
+ const chalk = require('chalk');
9
+ const ora = require('ora');
10
+ const prompts = require('prompts');
11
+
12
+ const TEMPLATE_DIR = path.join(__dirname, '..', 'templates');
13
+
14
+ // ─── Package JSON builder ─────────────────────────────────────────────────────
15
+ function buildPackageJson(projectName, useTs) {
16
+ const base = {
17
+ name: projectName,
18
+ version: '1.0.0',
19
+ description: 'Node.js + Prisma + Neon project',
20
+ dependencies: {
21
+ '@prisma/adapter-neon': '^6.0.0',
22
+ '@prisma/client': '^6.0.0',
23
+ dotenv: '^16.0.0',
24
+ express: '^4.18.0',
25
+ prisma: '^6.0.0',
26
+ },
27
+ };
28
+
29
+ if (useTs) {
30
+ return {
31
+ ...base,
32
+ main: 'dist/index.js',
33
+ scripts: {
34
+ dev: 'ts-node src/index.ts',
35
+ build: 'tsc',
36
+ start: 'node dist/index.js',
37
+ migrate: 'prisma migrate dev',
38
+ seed: 'ts-node prisma/seed.ts',
39
+ studio: 'prisma studio',
40
+ postinstall: 'prisma generate',
41
+ },
42
+ devDependencies: {
43
+ '@types/express': '^4.17.0',
44
+ '@types/node': '^20.0.0',
45
+ 'ts-node': '^10.9.0',
46
+ typescript: '^5.0.0',
47
+ },
48
+ };
49
+ }
50
+
51
+ return {
52
+ ...base,
53
+ main: 'src/index.js',
54
+ scripts: {
55
+ dev: 'nodemon src/index.js',
56
+ start: 'node src/index.js',
57
+ migrate: 'prisma migrate dev',
58
+ seed: 'node prisma/seed.js',
59
+ studio: 'prisma studio',
60
+ postinstall: 'prisma generate',
61
+ },
62
+ devDependencies: {
63
+ nodemon: '^3.0.0',
64
+ },
65
+ };
66
+ }
67
+
68
+ // ─── tsconfig ─────────────────────────────────────────────────────────────────
69
+ const TSCONFIG = JSON.stringify({
70
+ compilerOptions: {
71
+ target: 'ES2020',
72
+ module: 'commonjs',
73
+ lib: ['ES2020'],
74
+ outDir: './dist',
75
+ rootDir: './src',
76
+ strict: true,
77
+ esModuleInterop: true,
78
+ skipLibCheck: true,
79
+ forceConsistentCasingInFileNames: true,
80
+ resolveJsonModule: true,
81
+ },
82
+ include: ['src/**/*'],
83
+ exclude: ['node_modules', 'dist'],
84
+ }, null, 2);
85
+
86
+ // ─── TypeScript templates ─────────────────────────────────────────────────────
87
+ const TS_DB = `import 'dotenv/config';
88
+ import { PrismaClient } from './generated/prisma';
89
+ import { PrismaNeon } from '@prisma/adapter-neon';
90
+
91
+ const adapter = new PrismaNeon({
92
+ connectionString: process.env.DATABASE_URL as string,
93
+ });
94
+
95
+ export const prisma = new PrismaClient({ adapter });
96
+ `;
97
+
98
+ const TS_INDEX = `import 'dotenv/config';
99
+ import express, { Request, Response } from 'express';
100
+ import { prisma } from './db';
101
+
102
+ const app = express();
103
+ app.use(express.json());
104
+
105
+ app.get('/', (_req: Request, res: Response) => {
106
+ res.json({ status: 'ok', message: 'neon-prisma-starter API is running 🚀' });
107
+ });
108
+
109
+ // ─── Authors ──────────────────────────────────────────────────────────────────
110
+ app.get('/authors', async (_req: Request, res: Response) => {
111
+ try {
112
+ const authors = await prisma.author.findMany({
113
+ include: { books: true },
114
+ orderBy: { createdAt: 'desc' },
115
+ });
116
+ res.json(authors);
117
+ } catch (err: any) { res.status(500).json({ error: err.message }); }
118
+ });
119
+
120
+ app.get('/authors/:id', async (req: Request, res: Response) => {
121
+ try {
122
+ const author = await prisma.author.findUnique({
123
+ where: { id: Number(req.params.id) },
124
+ include: { books: true },
125
+ });
126
+ if (!author) return res.status(404).json({ error: 'Author not found' });
127
+ res.json(author);
128
+ } catch (err: any) { res.status(500).json({ error: err.message }); }
129
+ });
130
+
131
+ app.post('/authors', async (req: Request, res: Response) => {
132
+ try {
133
+ const { name, bio }: { name: string; bio?: string } = req.body;
134
+ if (!name) return res.status(400).json({ error: 'name is required' });
135
+ const author = await prisma.author.create({ data: { name, bio } });
136
+ res.status(201).json(author);
137
+ } catch (err: any) { res.status(500).json({ error: err.message }); }
138
+ });
139
+
140
+ app.delete('/authors/:id', async (req: Request, res: Response) => {
141
+ try {
142
+ await prisma.book.deleteMany({ where: { authorId: Number(req.params.id) } });
143
+ await prisma.author.delete({ where: { id: Number(req.params.id) } });
144
+ res.json({ message: 'Author deleted' });
145
+ } catch (err: any) { res.status(500).json({ error: err.message }); }
146
+ });
147
+
148
+ // ─── Books ────────────────────────────────────────────────────────────────────
149
+ app.get('/books', async (_req: Request, res: Response) => {
150
+ try {
151
+ const books = await prisma.book.findMany({
152
+ include: { author: true },
153
+ orderBy: { createdAt: 'desc' },
154
+ });
155
+ res.json(books);
156
+ } catch (err: any) { res.status(500).json({ error: err.message }); }
157
+ });
158
+
159
+ app.post('/books', async (req: Request, res: Response) => {
160
+ try {
161
+ const { title, genre, authorId }: { title: string; genre?: string; authorId: number } = req.body;
162
+ if (!title || !authorId) return res.status(400).json({ error: 'title and authorId are required' });
163
+ const book = await prisma.book.create({
164
+ data: { title, genre, authorId: Number(authorId) },
165
+ include: { author: true },
166
+ });
167
+ res.status(201).json(book);
168
+ } catch (err: any) { res.status(500).json({ error: err.message }); }
169
+ });
170
+
171
+ app.delete('/books/:id', async (req: Request, res: Response) => {
172
+ try {
173
+ await prisma.book.delete({ where: { id: Number(req.params.id) } });
174
+ res.json({ message: 'Book deleted' });
175
+ } catch (err: any) { res.status(500).json({ error: err.message }); }
176
+ });
177
+
178
+ const PORT = process.env.PORT || 3000;
179
+ app.listen(PORT, () => {
180
+ console.log(\`\\n🚀 Server running at http://localhost:\${PORT}\\n\`);
181
+ });
182
+ `;
183
+
184
+ const TS_SEED = `import { prisma } from '../src/db';
185
+
186
+ async function seed() {
187
+ console.log('🌱 Seeding database...');
188
+
189
+ await prisma.book.deleteMany();
190
+ await prisma.author.deleteMany();
191
+
192
+ await prisma.author.create({
193
+ data: {
194
+ name: 'J.R.R. Tolkien',
195
+ bio: 'Creator of Middle-earth.',
196
+ books: {
197
+ create: [
198
+ { title: 'The Hobbit', genre: 'Fantasy' },
199
+ { title: 'The Fellowship of the Ring', genre: 'Fantasy' },
200
+ { title: 'The Two Towers', genre: 'Fantasy' },
201
+ ],
202
+ },
203
+ },
204
+ });
205
+
206
+ await prisma.author.create({
207
+ data: {
208
+ name: 'George R.R. Martin',
209
+ bio: 'Author of A Song of Ice and Fire.',
210
+ books: {
211
+ create: [
212
+ { title: 'A Game of Thrones', genre: 'Fantasy' },
213
+ { title: 'A Clash of Kings', genre: 'Fantasy' },
214
+ ],
215
+ },
216
+ },
217
+ });
218
+
219
+ console.log('✅ Database seeded!');
220
+ }
221
+
222
+ seed()
223
+ .catch((err) => { console.error('❌ Seed failed:', err); process.exit(1); })
224
+ .finally(() => prisma.$disconnect());
225
+ `;
226
+
227
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
228
+ function run(cmd, cwd) {
229
+ execSync(cmd, { stdio: 'inherit', cwd });
230
+ }
231
+
232
+ function writeFile(filePath, content) {
233
+ fs.ensureDirSync(path.dirname(filePath));
234
+ fs.writeFileSync(filePath, content, 'utf8');
235
+ }
236
+
237
+ // ─── Main ─────────────────────────────────────────────────────────────────────
238
+ async function main() {
239
+ console.log('\n');
240
+ console.log(chalk.bgGreen.black.bold(' neon-prisma-starter '));
241
+ console.log(chalk.gray(' Node.js + Prisma + Neon · Instant Project Setup\n'));
242
+
243
+ // 1. Project name
244
+ let projectName = process.argv[2];
245
+ if (!projectName) {
246
+ const res = await prompts({
247
+ type: 'text',
248
+ name: 'projectName',
249
+ message: 'Project name:',
250
+ initial: 'my-neon-app',
251
+ validate: v => v.trim().length > 0 || 'Project name cannot be empty',
252
+ });
253
+ projectName = res.projectName;
254
+ }
255
+
256
+ if (!projectName) {
257
+ console.log(chalk.red('\n✖ No project name provided. Exiting.\n'));
258
+ process.exit(1);
259
+ }
260
+
261
+ const targetDir = path.resolve(process.cwd(), projectName);
262
+
263
+ // 2. Overwrite check
264
+ if (fs.existsSync(targetDir)) {
265
+ const { overwrite } = await prompts({
266
+ type: 'confirm',
267
+ name: 'overwrite',
268
+ message: `Folder "${projectName}" already exists. Overwrite?`,
269
+ initial: false,
270
+ });
271
+ if (!overwrite) { console.log(chalk.yellow('\n⚠ Aborted.\n')); process.exit(0); }
272
+ fs.removeSync(targetDir);
273
+ }
274
+
275
+ // 3. ── Language choice ──────────────────────────────────────────────────────
276
+ const { language } = await prompts({
277
+ type: 'select',
278
+ name: 'language',
279
+ message: 'Which language do you want to use?',
280
+ choices: [
281
+ {
282
+ title: chalk.blue('TypeScript') + chalk.gray(' (recommended)'),
283
+ description: 'Full type safety, tsconfig included, ts-node dev setup',
284
+ value: 'ts',
285
+ },
286
+ {
287
+ title: chalk.green('JavaScript'),
288
+ description: 'Plain JS with nodemon dev setup',
289
+ value: 'js',
290
+ },
291
+ ],
292
+ initial: 0,
293
+ });
294
+
295
+ if (!language) { console.log(chalk.yellow('\n⚠ Aborted.\n')); process.exit(0); }
296
+ const useTs = language === 'ts';
297
+
298
+ // 4. DATABASE_URL
299
+ const { dbUrl } = await prompts({
300
+ type: 'text',
301
+ name: 'dbUrl',
302
+ message: 'Paste your Neon DATABASE_URL (or press Enter to fill in later):',
303
+ initial: '',
304
+ });
305
+
306
+ console.log('');
307
+
308
+ // ─── Scaffold ────────────────────────────────────────────────────────────
309
+ const spinner = ora(`Scaffolding ${useTs ? 'TypeScript' : 'JavaScript'} project...`).start();
310
+
311
+ try {
312
+ fs.ensureDirSync(targetDir);
313
+
314
+ // Shared: prisma schema
315
+ fs.copySync(
316
+ path.join(TEMPLATE_DIR, 'prisma', 'schema.prisma'),
317
+ path.join(targetDir, 'prisma', 'schema.prisma')
318
+ );
319
+
320
+ if (useTs) {
321
+ writeFile(path.join(targetDir, 'src', 'db.ts'), TS_DB);
322
+ writeFile(path.join(targetDir, 'src', 'index.ts'), TS_INDEX);
323
+ writeFile(path.join(targetDir, 'prisma', 'seed.ts'), TS_SEED);
324
+ writeFile(path.join(targetDir, 'tsconfig.json'), TSCONFIG);
325
+ } else {
326
+ fs.copySync(path.join(TEMPLATE_DIR, 'src', 'db.js'), path.join(targetDir, 'src', 'db.js'));
327
+ fs.copySync(path.join(TEMPLATE_DIR, 'src', 'index.js'), path.join(targetDir, 'src', 'index.js'));
328
+ fs.copySync(path.join(TEMPLATE_DIR, 'prisma', 'seed.js'), path.join(targetDir, 'prisma', 'seed.js'));
329
+ }
330
+
331
+ // .env
332
+ const envContent = dbUrl
333
+ ? `DATABASE_URL="${dbUrl}"\n`
334
+ : `DATABASE_URL="postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/neondb?sslmode=require"\n`;
335
+ writeFile(path.join(targetDir, '.env'), envContent);
336
+ writeFile(path.join(targetDir, '.env.example'),
337
+ `DATABASE_URL="postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/neondb?sslmode=require"\n`);
338
+
339
+ // .gitignore
340
+ fs.copySync(path.join(TEMPLATE_DIR, '.gitignore'), path.join(targetDir, '.gitignore'));
341
+
342
+ // package.json
343
+ writeFile(
344
+ path.join(targetDir, 'package.json'),
345
+ JSON.stringify(buildPackageJson(projectName, useTs), null, 2)
346
+ );
347
+
348
+ spinner.succeed(chalk.green(`${useTs ? 'TypeScript' : 'JavaScript'} project scaffolded`));
349
+ } catch (err) {
350
+ spinner.fail('Failed to scaffold project');
351
+ console.error(err);
352
+ process.exit(1);
353
+ }
354
+
355
+ // ─── Install deps ─────────────────────────────────────────────────────────
356
+ const installSpinner = ora('Installing dependencies...').start();
357
+ try {
358
+ run('npm install', targetDir);
359
+ installSpinner.succeed(chalk.green('Dependencies installed'));
360
+ } catch {
361
+ installSpinner.fail('npm install failed — run it manually');
362
+ }
363
+
364
+ // ─── Migrate ──────────────────────────────────────────────────────────────
365
+ if (dbUrl) {
366
+ const migrateSpinner = ora('Running Prisma migration...').start();
367
+ try {
368
+ run('npx prisma migrate dev --name init', targetDir);
369
+ migrateSpinner.succeed(chalk.green('Database migrated'));
370
+ } catch {
371
+ migrateSpinner.warn(chalk.yellow('Migration skipped — run: npm run migrate'));
372
+ }
373
+ }
374
+
375
+ // ─── Done ─────────────────────────────────────────────────────────────────
376
+ const langBadge = useTs
377
+ ? chalk.bgBlue.white.bold(' TypeScript ')
378
+ : chalk.bgGreen.black.bold(' JavaScript ');
379
+
380
+ console.log('\n' + chalk.bgGreen.black.bold(' ✅ Project ready! ') + ' ' + langBadge + '\n');
381
+ console.log(chalk.white.bold(` cd ${projectName}`));
382
+ console.log('');
383
+
384
+ if (!dbUrl) {
385
+ console.log(chalk.yellow(' ⚠ Add your Neon DATABASE_URL to .env, then:'));
386
+ console.log('');
387
+ }
388
+
389
+ console.log(chalk.cyan(' npm run migrate') + chalk.gray(' → create tables in Neon'));
390
+ console.log(chalk.cyan(' npm run seed ') + chalk.gray(' → populate with example data'));
391
+ console.log(chalk.cyan(' npm run dev ') + chalk.gray(' → start server on :3000'));
392
+ if (useTs) {
393
+ console.log(chalk.cyan(' npm run build ') + chalk.gray(' → compile TypeScript → dist/'));
394
+ }
395
+ console.log('');
396
+ console.log(chalk.gray(' Endpoints: GET /authors POST /authors GET /books POST /books'));
397
+ console.log('');
398
+ }
399
+
400
+ main().catch(err => {
401
+ console.error(chalk.red('\n✖ Something went wrong:\n'), err);
402
+ process.exit(1);
403
+ });
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "neon-prisma-starter",
3
+ "version": "1.0.0",
4
+ "description": "CLI to scaffold a Node.js + Prisma + Neon project instantly",
5
+ "main": "bin/index.js",
6
+ "bin": {
7
+ "neon-prisma-starter": "bin/index.js",
8
+ "create-neon-app": "bin/index.js"
9
+ },
10
+ "scripts": {
11
+ "test": "node bin/index.js test-project"
12
+ },
13
+ "keywords": [
14
+ "neon",
15
+ "prisma",
16
+ "nodejs",
17
+ "express",
18
+ "postgresql",
19
+ "starter",
20
+ "cli",
21
+ "scaffold"
22
+ ],
23
+ "author": "",
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "chalk": "^4.1.2",
27
+ "fs-extra": "^11.2.0",
28
+ "ora": "^5.4.1",
29
+ "prompts": "^2.4.2"
30
+ }
31
+ }
@@ -0,0 +1,27 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ output = "../src/generated/prisma"
4
+ previewFeatures = ["driverAdapters"]
5
+ }
6
+
7
+ datasource db {
8
+ provider = "postgresql"
9
+ url = env("DATABASE_URL")
10
+ }
11
+
12
+ model Author {
13
+ id Int @id @default(autoincrement())
14
+ name String
15
+ bio String?
16
+ books Book[]
17
+ createdAt DateTime @default(now())
18
+ }
19
+
20
+ model Book {
21
+ id Int @id @default(autoincrement())
22
+ title String
23
+ genre String?
24
+ author Author @relation(fields: [authorId], references: [id])
25
+ authorId Int
26
+ createdAt DateTime @default(now())
27
+ }
@@ -0,0 +1,62 @@
1
+ const { prisma } = require('../src/db');
2
+
3
+ async function seed() {
4
+ console.log('🌱 Seeding database...');
5
+
6
+ // Clean existing data
7
+ await prisma.book.deleteMany();
8
+ await prisma.author.deleteMany();
9
+
10
+ // Seed authors with books
11
+ await prisma.author.create({
12
+ data: {
13
+ name: 'J.R.R. Tolkien',
14
+ bio: 'Creator of Middle-earth and author of The Lord of the Rings.',
15
+ books: {
16
+ create: [
17
+ { title: 'The Hobbit', genre: 'Fantasy' },
18
+ { title: 'The Fellowship of the Ring', genre: 'Fantasy' },
19
+ { title: 'The Two Towers', genre: 'Fantasy' },
20
+ { title: 'The Return of the King', genre: 'Fantasy' },
21
+ ],
22
+ },
23
+ },
24
+ });
25
+
26
+ await prisma.author.create({
27
+ data: {
28
+ name: 'George R.R. Martin',
29
+ bio: 'Author of the epic fantasy series A Song of Ice and Fire.',
30
+ books: {
31
+ create: [
32
+ { title: 'A Game of Thrones', genre: 'Fantasy' },
33
+ { title: 'A Clash of Kings', genre: 'Fantasy' },
34
+ { title: 'A Storm of Swords', genre: 'Fantasy' },
35
+ ],
36
+ },
37
+ },
38
+ });
39
+
40
+ await prisma.author.create({
41
+ data: {
42
+ name: 'Frank Herbert',
43
+ bio: 'Author of the Dune science fiction series.',
44
+ books: {
45
+ create: [
46
+ { title: 'Dune', genre: 'Sci-Fi' },
47
+ { title: 'Dune Messiah', genre: 'Sci-Fi' },
48
+ { title: 'Children of Dune', genre: 'Sci-Fi' },
49
+ ],
50
+ },
51
+ },
52
+ });
53
+
54
+ console.log('✅ Database seeded successfully!');
55
+ }
56
+
57
+ seed()
58
+ .catch(err => {
59
+ console.error('❌ Seed failed:', err);
60
+ process.exit(1);
61
+ })
62
+ .finally(() => prisma.$disconnect());
@@ -0,0 +1,11 @@
1
+ require('dotenv').config();
2
+ const { PrismaClient } = require('./generated/prisma');
3
+ const { PrismaNeon } = require('@prisma/adapter-neon');
4
+
5
+ const adapter = new PrismaNeon({
6
+ connectionString: process.env.DATABASE_URL,
7
+ });
8
+
9
+ const prisma = new PrismaClient({ adapter });
10
+
11
+ module.exports = { prisma };
@@ -0,0 +1,101 @@
1
+ require('dotenv').config();
2
+ const express = require('express');
3
+ const { prisma } = require('./db');
4
+
5
+ const app = express();
6
+ app.use(express.json());
7
+
8
+ // ─── Health ───────────────────────────────────────────────────────────────────
9
+ app.get('/', (req, res) => {
10
+ res.json({ status: 'ok', message: 'neon-prisma-starter API is running 🚀' });
11
+ });
12
+
13
+ // ─── Authors ──────────────────────────────────────────────────────────────────
14
+ app.get('/authors', async (req, res) => {
15
+ try {
16
+ const authors = await prisma.author.findMany({
17
+ include: { books: true },
18
+ orderBy: { createdAt: 'desc' },
19
+ });
20
+ res.json(authors);
21
+ } catch (err) {
22
+ res.status(500).json({ error: err.message });
23
+ }
24
+ });
25
+
26
+ app.get('/authors/:id', async (req, res) => {
27
+ try {
28
+ const author = await prisma.author.findUnique({
29
+ where: { id: Number(req.params.id) },
30
+ include: { books: true },
31
+ });
32
+ if (!author) return res.status(404).json({ error: 'Author not found' });
33
+ res.json(author);
34
+ } catch (err) {
35
+ res.status(500).json({ error: err.message });
36
+ }
37
+ });
38
+
39
+ app.post('/authors', async (req, res) => {
40
+ try {
41
+ const { name, bio } = req.body;
42
+ if (!name) return res.status(400).json({ error: 'name is required' });
43
+ const author = await prisma.author.create({ data: { name, bio } });
44
+ res.status(201).json(author);
45
+ } catch (err) {
46
+ res.status(500).json({ error: err.message });
47
+ }
48
+ });
49
+
50
+ app.delete('/authors/:id', async (req, res) => {
51
+ try {
52
+ await prisma.book.deleteMany({ where: { authorId: Number(req.params.id) } });
53
+ await prisma.author.delete({ where: { id: Number(req.params.id) } });
54
+ res.json({ message: 'Author deleted' });
55
+ } catch (err) {
56
+ res.status(500).json({ error: err.message });
57
+ }
58
+ });
59
+
60
+ // ─── Books ────────────────────────────────────────────────────────────────────
61
+ app.get('/books', async (req, res) => {
62
+ try {
63
+ const books = await prisma.book.findMany({
64
+ include: { author: true },
65
+ orderBy: { createdAt: 'desc' },
66
+ });
67
+ res.json(books);
68
+ } catch (err) {
69
+ res.status(500).json({ error: err.message });
70
+ }
71
+ });
72
+
73
+ app.post('/books', async (req, res) => {
74
+ try {
75
+ const { title, genre, authorId } = req.body;
76
+ if (!title || !authorId) return res.status(400).json({ error: 'title and authorId are required' });
77
+ const book = await prisma.book.create({
78
+ data: { title, genre, authorId: Number(authorId) },
79
+ include: { author: true },
80
+ });
81
+ res.status(201).json(book);
82
+ } catch (err) {
83
+ res.status(500).json({ error: err.message });
84
+ }
85
+ });
86
+
87
+ app.delete('/books/:id', async (req, res) => {
88
+ try {
89
+ await prisma.book.delete({ where: { id: Number(req.params.id) } });
90
+ res.json({ message: 'Book deleted' });
91
+ } catch (err) {
92
+ res.status(500).json({ error: err.message });
93
+ }
94
+ });
95
+
96
+ // ─── Start ────────────────────────────────────────────────────────────────────
97
+ const PORT = process.env.PORT || 3000;
98
+ app.listen(PORT, () => {
99
+ console.log(`\n🚀 Server running at http://localhost:${PORT}`);
100
+ console.log(`📦 Routes: GET /authors POST /authors GET /books POST /books\n`);
101
+ });
@@ -0,0 +1,27 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ output = "../src/generated/prisma"
4
+ previewFeatures = ["driverAdapters"]
5
+ }
6
+
7
+ datasource db {
8
+ provider = "postgresql"
9
+ url = env("DATABASE_URL")
10
+ }
11
+
12
+ model Author {
13
+ id Int @id @default(autoincrement())
14
+ name String
15
+ bio String?
16
+ books Book[]
17
+ createdAt DateTime @default(now())
18
+ }
19
+
20
+ model Book {
21
+ id Int @id @default(autoincrement())
22
+ title String
23
+ genre String?
24
+ author Author @relation(fields: [authorId], references: [id])
25
+ authorId Int
26
+ createdAt DateTime @default(now())
27
+ }
@@ -0,0 +1,62 @@
1
+ const { prisma } = require('../src/db');
2
+
3
+ async function seed() {
4
+ console.log('🌱 Seeding database...');
5
+
6
+ // Clean existing data
7
+ await prisma.book.deleteMany();
8
+ await prisma.author.deleteMany();
9
+
10
+ // Seed authors with books
11
+ await prisma.author.create({
12
+ data: {
13
+ name: 'J.R.R. Tolkien',
14
+ bio: 'Creator of Middle-earth and author of The Lord of the Rings.',
15
+ books: {
16
+ create: [
17
+ { title: 'The Hobbit', genre: 'Fantasy' },
18
+ { title: 'The Fellowship of the Ring', genre: 'Fantasy' },
19
+ { title: 'The Two Towers', genre: 'Fantasy' },
20
+ { title: 'The Return of the King', genre: 'Fantasy' },
21
+ ],
22
+ },
23
+ },
24
+ });
25
+
26
+ await prisma.author.create({
27
+ data: {
28
+ name: 'George R.R. Martin',
29
+ bio: 'Author of the epic fantasy series A Song of Ice and Fire.',
30
+ books: {
31
+ create: [
32
+ { title: 'A Game of Thrones', genre: 'Fantasy' },
33
+ { title: 'A Clash of Kings', genre: 'Fantasy' },
34
+ { title: 'A Storm of Swords', genre: 'Fantasy' },
35
+ ],
36
+ },
37
+ },
38
+ });
39
+
40
+ await prisma.author.create({
41
+ data: {
42
+ name: 'Frank Herbert',
43
+ bio: 'Author of the Dune science fiction series.',
44
+ books: {
45
+ create: [
46
+ { title: 'Dune', genre: 'Sci-Fi' },
47
+ { title: 'Dune Messiah', genre: 'Sci-Fi' },
48
+ { title: 'Children of Dune', genre: 'Sci-Fi' },
49
+ ],
50
+ },
51
+ },
52
+ });
53
+
54
+ console.log('✅ Database seeded successfully!');
55
+ }
56
+
57
+ seed()
58
+ .catch(err => {
59
+ console.error('❌ Seed failed:', err);
60
+ process.exit(1);
61
+ })
62
+ .finally(() => prisma.$disconnect());
@@ -0,0 +1,11 @@
1
+ require('dotenv').config();
2
+ const { PrismaClient } = require('./generated/prisma');
3
+ const { PrismaNeon } = require('@prisma/adapter-neon');
4
+
5
+ const adapter = new PrismaNeon({
6
+ connectionString: process.env.DATABASE_URL,
7
+ });
8
+
9
+ const prisma = new PrismaClient({ adapter });
10
+
11
+ module.exports = { prisma };
@@ -0,0 +1,101 @@
1
+ require('dotenv').config();
2
+ const express = require('express');
3
+ const { prisma } = require('./db');
4
+
5
+ const app = express();
6
+ app.use(express.json());
7
+
8
+ // ─── Health ───────────────────────────────────────────────────────────────────
9
+ app.get('/', (req, res) => {
10
+ res.json({ status: 'ok', message: 'neon-prisma-starter API is running 🚀' });
11
+ });
12
+
13
+ // ─── Authors ──────────────────────────────────────────────────────────────────
14
+ app.get('/authors', async (req, res) => {
15
+ try {
16
+ const authors = await prisma.author.findMany({
17
+ include: { books: true },
18
+ orderBy: { createdAt: 'desc' },
19
+ });
20
+ res.json(authors);
21
+ } catch (err) {
22
+ res.status(500).json({ error: err.message });
23
+ }
24
+ });
25
+
26
+ app.get('/authors/:id', async (req, res) => {
27
+ try {
28
+ const author = await prisma.author.findUnique({
29
+ where: { id: Number(req.params.id) },
30
+ include: { books: true },
31
+ });
32
+ if (!author) return res.status(404).json({ error: 'Author not found' });
33
+ res.json(author);
34
+ } catch (err) {
35
+ res.status(500).json({ error: err.message });
36
+ }
37
+ });
38
+
39
+ app.post('/authors', async (req, res) => {
40
+ try {
41
+ const { name, bio } = req.body;
42
+ if (!name) return res.status(400).json({ error: 'name is required' });
43
+ const author = await prisma.author.create({ data: { name, bio } });
44
+ res.status(201).json(author);
45
+ } catch (err) {
46
+ res.status(500).json({ error: err.message });
47
+ }
48
+ });
49
+
50
+ app.delete('/authors/:id', async (req, res) => {
51
+ try {
52
+ await prisma.book.deleteMany({ where: { authorId: Number(req.params.id) } });
53
+ await prisma.author.delete({ where: { id: Number(req.params.id) } });
54
+ res.json({ message: 'Author deleted' });
55
+ } catch (err) {
56
+ res.status(500).json({ error: err.message });
57
+ }
58
+ });
59
+
60
+ // ─── Books ────────────────────────────────────────────────────────────────────
61
+ app.get('/books', async (req, res) => {
62
+ try {
63
+ const books = await prisma.book.findMany({
64
+ include: { author: true },
65
+ orderBy: { createdAt: 'desc' },
66
+ });
67
+ res.json(books);
68
+ } catch (err) {
69
+ res.status(500).json({ error: err.message });
70
+ }
71
+ });
72
+
73
+ app.post('/books', async (req, res) => {
74
+ try {
75
+ const { title, genre, authorId } = req.body;
76
+ if (!title || !authorId) return res.status(400).json({ error: 'title and authorId are required' });
77
+ const book = await prisma.book.create({
78
+ data: { title, genre, authorId: Number(authorId) },
79
+ include: { author: true },
80
+ });
81
+ res.status(201).json(book);
82
+ } catch (err) {
83
+ res.status(500).json({ error: err.message });
84
+ }
85
+ });
86
+
87
+ app.delete('/books/:id', async (req, res) => {
88
+ try {
89
+ await prisma.book.delete({ where: { id: Number(req.params.id) } });
90
+ res.json({ message: 'Book deleted' });
91
+ } catch (err) {
92
+ res.status(500).json({ error: err.message });
93
+ }
94
+ });
95
+
96
+ // ─── Start ────────────────────────────────────────────────────────────────────
97
+ const PORT = process.env.PORT || 3000;
98
+ app.listen(PORT, () => {
99
+ console.log(`\n🚀 Server running at http://localhost:${PORT}`);
100
+ console.log(`📦 Routes: GET /authors POST /authors GET /books POST /books\n`);
101
+ });
@@ -0,0 +1,27 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ output = "../src/generated/prisma"
4
+ previewFeatures = ["driverAdapters"]
5
+ }
6
+
7
+ datasource db {
8
+ provider = "postgresql"
9
+ url = env("DATABASE_URL")
10
+ }
11
+
12
+ model Author {
13
+ id Int @id @default(autoincrement())
14
+ name String
15
+ bio String?
16
+ books Book[]
17
+ createdAt DateTime @default(now())
18
+ }
19
+
20
+ model Book {
21
+ id Int @id @default(autoincrement())
22
+ title String
23
+ genre String?
24
+ author Author @relation(fields: [authorId], references: [id])
25
+ authorId Int
26
+ createdAt DateTime @default(now())
27
+ }
@@ -0,0 +1,61 @@
1
+ import { prisma } from '../src/db';
2
+
3
+ async function seed(): Promise<void> {
4
+ console.log('🌱 Seeding database...');
5
+
6
+ // Clean existing data
7
+ await prisma.book.deleteMany();
8
+ await prisma.author.deleteMany();
9
+
10
+ await prisma.author.create({
11
+ data: {
12
+ name: 'J.R.R. Tolkien',
13
+ bio: 'Creator of Middle-earth and author of The Lord of the Rings.',
14
+ books: {
15
+ create: [
16
+ { title: 'The Hobbit', genre: 'Fantasy' },
17
+ { title: 'The Fellowship of the Ring', genre: 'Fantasy' },
18
+ { title: 'The Two Towers', genre: 'Fantasy' },
19
+ { title: 'The Return of the King', genre: 'Fantasy' },
20
+ ],
21
+ },
22
+ },
23
+ });
24
+
25
+ await prisma.author.create({
26
+ data: {
27
+ name: 'George R.R. Martin',
28
+ bio: 'Author of the epic fantasy series A Song of Ice and Fire.',
29
+ books: {
30
+ create: [
31
+ { title: 'A Game of Thrones', genre: 'Fantasy' },
32
+ { title: 'A Clash of Kings', genre: 'Fantasy' },
33
+ { title: 'A Storm of Swords', genre: 'Fantasy' },
34
+ ],
35
+ },
36
+ },
37
+ });
38
+
39
+ await prisma.author.create({
40
+ data: {
41
+ name: 'Frank Herbert',
42
+ bio: 'Author of the Dune science fiction series.',
43
+ books: {
44
+ create: [
45
+ { title: 'Dune', genre: 'Sci-Fi' },
46
+ { title: 'Dune Messiah', genre: 'Sci-Fi' },
47
+ { title: 'Children of Dune', genre: 'Sci-Fi' },
48
+ ],
49
+ },
50
+ },
51
+ });
52
+
53
+ console.log('✅ Database seeded successfully!');
54
+ }
55
+
56
+ seed()
57
+ .catch((err: Error) => {
58
+ console.error('❌ Seed failed:', err);
59
+ process.exit(1);
60
+ })
61
+ .finally(() => prisma.$disconnect());
@@ -0,0 +1,9 @@
1
+ import 'dotenv/config';
2
+ import { PrismaClient } from './generated/prisma';
3
+ import { PrismaNeon } from '@prisma/adapter-neon';
4
+
5
+ const adapter = new PrismaNeon({
6
+ connectionString: process.env.DATABASE_URL as string,
7
+ });
8
+
9
+ export const prisma = new PrismaClient({ adapter });
@@ -0,0 +1,101 @@
1
+ import 'dotenv/config';
2
+ import express, { Request, Response } from 'express';
3
+ import { prisma } from './db';
4
+
5
+ const app = express();
6
+ app.use(express.json());
7
+
8
+ // ─── Health ───────────────────────────────────────────────────────────────────
9
+ app.get('/', (_req: Request, res: Response) => {
10
+ res.json({ status: 'ok', message: 'neon-prisma-starter API is running 🚀' });
11
+ });
12
+
13
+ // ─── Authors ──────────────────────────────────────────────────────────────────
14
+ app.get('/authors', async (_req: Request, res: Response) => {
15
+ try {
16
+ const authors = await prisma.author.findMany({
17
+ include: { books: true },
18
+ orderBy: { createdAt: 'desc' },
19
+ });
20
+ res.json(authors);
21
+ } catch (err) {
22
+ res.status(500).json({ error: (err as Error).message });
23
+ }
24
+ });
25
+
26
+ app.get('/authors/:id', async (req: Request, res: Response) => {
27
+ try {
28
+ const author = await prisma.author.findUnique({
29
+ where: { id: Number(req.params.id) },
30
+ include: { books: true },
31
+ });
32
+ if (!author) return res.status(404).json({ error: 'Author not found' });
33
+ res.json(author);
34
+ } catch (err) {
35
+ res.status(500).json({ error: (err as Error).message });
36
+ }
37
+ });
38
+
39
+ app.post('/authors', async (req: Request, res: Response) => {
40
+ try {
41
+ const { name, bio }: { name: string; bio?: string } = req.body;
42
+ if (!name) return res.status(400).json({ error: 'name is required' });
43
+ const author = await prisma.author.create({ data: { name, bio } });
44
+ res.status(201).json(author);
45
+ } catch (err) {
46
+ res.status(500).json({ error: (err as Error).message });
47
+ }
48
+ });
49
+
50
+ app.delete('/authors/:id', async (req: Request, res: Response) => {
51
+ try {
52
+ await prisma.book.deleteMany({ where: { authorId: Number(req.params.id) } });
53
+ await prisma.author.delete({ where: { id: Number(req.params.id) } });
54
+ res.json({ message: 'Author deleted' });
55
+ } catch (err) {
56
+ res.status(500).json({ error: (err as Error).message });
57
+ }
58
+ });
59
+
60
+ // ─── Books ────────────────────────────────────────────────────────────────────
61
+ app.get('/books', async (_req: Request, res: Response) => {
62
+ try {
63
+ const books = await prisma.book.findMany({
64
+ include: { author: true },
65
+ orderBy: { createdAt: 'desc' },
66
+ });
67
+ res.json(books);
68
+ } catch (err) {
69
+ res.status(500).json({ error: (err as Error).message });
70
+ }
71
+ });
72
+
73
+ app.post('/books', async (req: Request, res: Response) => {
74
+ try {
75
+ const { title, genre, authorId }: { title: string; genre?: string; authorId: number } = req.body;
76
+ if (!title || !authorId) return res.status(400).json({ error: 'title and authorId are required' });
77
+ const book = await prisma.book.create({
78
+ data: { title, genre, authorId: Number(authorId) },
79
+ include: { author: true },
80
+ });
81
+ res.status(201).json(book);
82
+ } catch (err) {
83
+ res.status(500).json({ error: (err as Error).message });
84
+ }
85
+ });
86
+
87
+ app.delete('/books/:id', async (req: Request, res: Response) => {
88
+ try {
89
+ await prisma.book.delete({ where: { id: Number(req.params.id) } });
90
+ res.json({ message: 'Book deleted' });
91
+ } catch (err) {
92
+ res.status(500).json({ error: (err as Error).message });
93
+ }
94
+ });
95
+
96
+ // ─── Start ────────────────────────────────────────────────────────────────────
97
+ const PORT = process.env.PORT || 3000;
98
+ app.listen(PORT, () => {
99
+ console.log(`\n🚀 Server running at http://localhost:${PORT}`);
100
+ console.log(`📦 Routes: GET /authors POST /authors GET /books POST /books\n`);
101
+ });
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist", "src/generated"]
19
+ }