659 lines
14 KiB
Markdown
659 lines
14 KiB
Markdown
---
|
|
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.**
|