Initial commit
This commit is contained in:
658
commands/express-api-scaffold.md
Normal file
658
commands/express-api-scaffold.md
Normal file
@@ -0,0 +1,658 @@
|
||||
---
|
||||
description: Generate production-ready Express.js REST API with TypeScript and auth
|
||||
shortcut: eas
|
||||
category: backend
|
||||
difficulty: intermediate
|
||||
estimated_time: 5-10 minutes
|
||||
---
|
||||
|
||||
# Express API Scaffold
|
||||
|
||||
Generates a complete Express.js REST API boilerplate with TypeScript, authentication, database integration, and testing setup.
|
||||
|
||||
## What This Command Does
|
||||
|
||||
**Generated Project:**
|
||||
- Express.js with TypeScript
|
||||
- JWT authentication
|
||||
- Database integration (Prisma or TypeORM)
|
||||
- Input validation (Zod)
|
||||
- Error handling middleware
|
||||
- Rate limiting & security (Helmet, CORS)
|
||||
- Testing setup (Jest + Supertest)
|
||||
- Docker configuration
|
||||
- Example CRUD endpoints
|
||||
|
||||
**Output:** Complete API project ready for development
|
||||
|
||||
**Time:** 5-10 minutes
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Generate full Express API
|
||||
/express-api-scaffold "Task Management API"
|
||||
|
||||
# Shortcut
|
||||
/eas "E-commerce API"
|
||||
|
||||
# With specific database
|
||||
/eas "Blog API" --database postgresql
|
||||
|
||||
# With authentication type
|
||||
/eas "Social API" --auth jwt --database mongodb
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Output
|
||||
|
||||
**Input:**
|
||||
```
|
||||
/eas "Task Management API" --database postgresql
|
||||
```
|
||||
|
||||
**Generated Project Structure:**
|
||||
```
|
||||
task-api/
|
||||
├── src/
|
||||
│ ├── controllers/ # Request handlers
|
||||
│ │ ├── auth.controller.ts
|
||||
│ │ └── task.controller.ts
|
||||
│ ├── middleware/ # Express middleware
|
||||
│ │ ├── auth.middleware.ts
|
||||
│ │ ├── error.middleware.ts
|
||||
│ │ └── validation.middleware.ts
|
||||
│ ├── models/ # Database models
|
||||
│ │ └── task.model.ts
|
||||
│ ├── routes/ # API routes
|
||||
│ │ ├── auth.routes.ts
|
||||
│ │ └── task.routes.ts
|
||||
│ ├── services/ # Business logic
|
||||
│ │ ├── auth.service.ts
|
||||
│ │ └── task.service.ts
|
||||
│ ├── utils/ # Utilities
|
||||
│ │ ├── jwt.util.ts
|
||||
│ │ └── password.util.ts
|
||||
│ ├── config/ # Configuration
|
||||
│ │ └── database.ts
|
||||
│ ├── types/ # TypeScript types
|
||||
│ │ └── express.d.ts
|
||||
│ ├── app.ts # Express app setup
|
||||
│ └── server.ts # Server entry point
|
||||
├── tests/
|
||||
│ ├── auth.test.ts
|
||||
│ └── task.test.ts
|
||||
├── prisma/
|
||||
│ └── schema.prisma # Database schema
|
||||
├── .env.example
|
||||
├── .gitignore
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── jest.config.js
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Generated Files
|
||||
|
||||
### 1. **src/server.ts** (Entry Point)
|
||||
|
||||
```typescript
|
||||
import app from './app'
|
||||
import { config } from './config'
|
||||
|
||||
const PORT = process.env.PORT || 3000
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`)
|
||||
console.log(`Environment: ${process.env.NODE_ENV}`)
|
||||
})
|
||||
```
|
||||
|
||||
### 2. **src/app.ts** (Express Setup)
|
||||
|
||||
```typescript
|
||||
import express, { Application } from 'express'
|
||||
import cors from 'cors'
|
||||
import helmet from 'helmet'
|
||||
import morgan from 'morgan'
|
||||
import rateLimit from 'express-rate-limit'
|
||||
|
||||
import authRoutes from './routes/auth.routes'
|
||||
import taskRoutes from './routes/task.routes'
|
||||
import { errorHandler } from './middleware/error.middleware'
|
||||
import { notFoundHandler } from './middleware/notFound.middleware'
|
||||
|
||||
const app: Application = express()
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet())
|
||||
app.use(cors({
|
||||
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
|
||||
credentials: true
|
||||
}))
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // 100 requests per window
|
||||
message: 'Too many requests, please try again later'
|
||||
})
|
||||
app.use('/api/', limiter)
|
||||
|
||||
// Parsing middleware
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
|
||||
// Logging
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
app.use(morgan('combined'))
|
||||
}
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() })
|
||||
})
|
||||
|
||||
// Routes
|
||||
app.use('/api/auth', authRoutes)
|
||||
app.use('/api/tasks', taskRoutes)
|
||||
|
||||
// Error handling
|
||||
app.use(notFoundHandler)
|
||||
app.use(errorHandler)
|
||||
|
||||
export default app
|
||||
```
|
||||
|
||||
### 3. **src/controllers/auth.controller.ts**
|
||||
|
||||
```typescript
|
||||
import { Request, Response, NextFunction } from 'express'
|
||||
import { AuthService } from '../services/auth.service'
|
||||
import { ApiError } from '../utils/ApiError'
|
||||
|
||||
const authService = new AuthService()
|
||||
|
||||
export class AuthController {
|
||||
async register(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { email, password, name } = req.body
|
||||
|
||||
const result = await authService.register({ email, password, name })
|
||||
|
||||
res.status(201).json({
|
||||
data: {
|
||||
user: result.user,
|
||||
token: result.token
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
async login(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { email, password } = req.body
|
||||
|
||||
const result = await authService.login(email, password)
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
user: result.user,
|
||||
token: result.token
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
async getProfile(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const userId = req.user!.id
|
||||
|
||||
const user = await authService.getUserById(userId)
|
||||
|
||||
res.json({ data: user })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. **src/middleware/auth.middleware.ts**
|
||||
|
||||
```typescript
|
||||
import { Request, Response, NextFunction } from 'express'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { ApiError } from '../utils/ApiError'
|
||||
|
||||
interface JwtPayload {
|
||||
userId: string
|
||||
email: string
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: {
|
||||
id: string
|
||||
email: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function authenticate(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const authHeader = req.headers.authorization
|
||||
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
throw new ApiError(401, 'No token provided')
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1]
|
||||
|
||||
const decoded = jwt.verify(
|
||||
token,
|
||||
process.env.JWT_SECRET!
|
||||
) as JwtPayload
|
||||
|
||||
req.user = {
|
||||
id: decoded.userId,
|
||||
email: decoded.email
|
||||
}
|
||||
|
||||
next()
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.JsonWebTokenError) {
|
||||
next(new ApiError(401, 'Invalid token'))
|
||||
} else {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. **src/middleware/error.middleware.ts**
|
||||
|
||||
```typescript
|
||||
import { Request, Response, NextFunction } from 'express'
|
||||
import { ApiError } from '../utils/ApiError'
|
||||
import { ZodError } from 'zod'
|
||||
|
||||
export function errorHandler(
|
||||
err: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
console.error('Error:', err)
|
||||
|
||||
// Handle known API errors
|
||||
if (err instanceof ApiError) {
|
||||
return res.status(err.statusCode).json({
|
||||
error: {
|
||||
code: err.name,
|
||||
message: err.message,
|
||||
...(err.details && { details: err.details })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Handle validation errors (Zod)
|
||||
if (err instanceof ZodError) {
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'Validation failed',
|
||||
details: err.errors.map(e => ({
|
||||
field: e.path.join('.'),
|
||||
message: e.message
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Handle unexpected errors
|
||||
res.status(500).json({
|
||||
error: {
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: process.env.NODE_ENV === 'production'
|
||||
? 'An unexpected error occurred'
|
||||
: err.message
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 6. **src/routes/task.routes.ts**
|
||||
|
||||
```typescript
|
||||
import { Router } from 'express'
|
||||
import { TaskController } from '../controllers/task.controller'
|
||||
import { authenticate } from '../middleware/auth.middleware'
|
||||
import { validate } from '../middleware/validation.middleware'
|
||||
import { createTaskSchema, updateTaskSchema } from '../schemas/task.schema'
|
||||
|
||||
const router = Router()
|
||||
const taskController = new TaskController()
|
||||
|
||||
// All routes require authentication
|
||||
router.use(authenticate)
|
||||
|
||||
router.get('/', taskController.list)
|
||||
router.post('/', validate(createTaskSchema), taskController.create)
|
||||
router.get('/:id', taskController.getById)
|
||||
router.patch('/:id', validate(updateTaskSchema), taskController.update)
|
||||
router.delete('/:id', taskController.delete)
|
||||
|
||||
export default router
|
||||
```
|
||||
|
||||
### 7. **src/services/task.service.ts**
|
||||
|
||||
```typescript
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import { ApiError } from '../utils/ApiError'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
export class TaskService {
|
||||
async create(userId: string, data: { title: string; description?: string }) {
|
||||
return await prisma.task.create({
|
||||
data: {
|
||||
...data,
|
||||
userId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async findAll(userId: string) {
|
||||
return await prisma.task.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
}
|
||||
|
||||
async findById(id: string, userId: string) {
|
||||
const task = await prisma.task.findUnique({
|
||||
where: { id }
|
||||
})
|
||||
|
||||
if (!task) {
|
||||
throw new ApiError(404, 'Task not found')
|
||||
}
|
||||
|
||||
if (task.userId !== userId) {
|
||||
throw new ApiError(403, 'Access denied')
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
async update(id: string, userId: string, data: Partial<{ title: string; description: string; completed: boolean }>) {
|
||||
await this.findById(id, userId) // Check ownership
|
||||
|
||||
return await prisma.task.update({
|
||||
where: { id },
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
async delete(id: string, userId: string) {
|
||||
await this.findById(id, userId) // Check ownership
|
||||
|
||||
await prisma.task.delete({
|
||||
where: { id }
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. **prisma/schema.prisma** (Database Schema)
|
||||
|
||||
```prisma
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
password String
|
||||
name String
|
||||
tasks Task[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model Task {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
description String?
|
||||
completed Boolean @default(false)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@map("tasks")
|
||||
}
|
||||
```
|
||||
|
||||
### 9. **tests/task.test.ts** (Integration Tests)
|
||||
|
||||
```typescript
|
||||
import request from 'supertest'
|
||||
import app from '../src/app'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
describe('Task API', () => {
|
||||
let authToken: string
|
||||
let userId: string
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create test user and get token
|
||||
const res = await request(app)
|
||||
.post('/api/auth/register')
|
||||
.send({
|
||||
email: '[email protected]',
|
||||
password: 'password123',
|
||||
name: 'Test User'
|
||||
})
|
||||
|
||||
authToken = res.body.data.token
|
||||
userId = res.body.data.user.id
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup
|
||||
await prisma.task.deleteMany({ where: { userId } })
|
||||
await prisma.user.delete({ where: { id: userId } })
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
|
||||
describe('POST /api/tasks', () => {
|
||||
it('should create a new task', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/tasks')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
title: 'Test Task',
|
||||
description: 'Test description'
|
||||
})
|
||||
|
||||
expect(res.status).toBe(201)
|
||||
expect(res.body.data).toHaveProperty('id')
|
||||
expect(res.body.data.title).toBe('Test Task')
|
||||
})
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/tasks')
|
||||
.send({ title: 'Test' })
|
||||
|
||||
expect(res.status).toBe(401)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/tasks', () => {
|
||||
it('should list user tasks', async () => {
|
||||
const res = await request(app)
|
||||
.get('/api/tasks')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
expect(Array.isArray(res.body.data)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 10. **package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "task-api",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "ts-node-dev --respawn --transpile-only src/server.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/server.js",
|
||||
"test": "jest --coverage",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:push": "prisma db push",
|
||||
"db:generate": "prisma generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"helmet": "^7.1.0",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"morgan": "^1.10.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"zod": "^3.22.4",
|
||||
"@prisma/client": "^5.8.0",
|
||||
"dotenv": "^16.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"typescript": "^5.3.3",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"supertest": "^6.3.3",
|
||||
"prisma": "^5.8.0",
|
||||
"eslint": "^8.56.0",
|
||||
"prettier": "^3.1.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
**Security:**
|
||||
- Helmet.js for HTTP headers
|
||||
- CORS with configurable origins
|
||||
- Rate limiting (100 req/15min)
|
||||
- JWT authentication
|
||||
- Password hashing (bcrypt)
|
||||
- Input validation (Zod)
|
||||
|
||||
**Database:**
|
||||
- Prisma ORM with TypeScript
|
||||
- Automatic migrations
|
||||
- Type-safe queries
|
||||
- Supports PostgreSQL, MySQL, SQLite
|
||||
|
||||
**Testing:**
|
||||
- Jest + Supertest
|
||||
- Integration tests
|
||||
- Coverage reporting
|
||||
- Test database isolation
|
||||
|
||||
**Development:**
|
||||
- Hot reload (ts-node-dev)
|
||||
- TypeScript with strict mode
|
||||
- ESLint + Prettier
|
||||
- Environment variables
|
||||
|
||||
**Production:**
|
||||
- Docker support
|
||||
- Health check endpoint
|
||||
- Error logging
|
||||
- Graceful shutdown
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
**1. Install dependencies:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
**2. Configure environment:**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your database URL and secrets
|
||||
```
|
||||
|
||||
**3. Run database migrations:**
|
||||
```bash
|
||||
npm run db:migrate
|
||||
```
|
||||
|
||||
**4. Start development server:**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
**5. Run tests:**
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Commands
|
||||
|
||||
- `/fastapi-scaffold` - Generate FastAPI boilerplate
|
||||
- Backend Architect (agent) - Architecture review
|
||||
- API Builder (agent) - API design guidance
|
||||
|
||||
---
|
||||
|
||||
**Build production-ready APIs. Ship faster. Scale confidently.**
|
||||
Reference in New Issue
Block a user