What: Express backend API for CodePatchwork
Tech: TypeScript + Express + PostgreSQL + Drizzle ORM + Firebase Admin SDK
# Install dependencies (from project root)
npm install
# Start dev server (runs both client and server)
npm run dev
# Server runs via tsx (TypeScript executor)
# Build server only
npm run build
# Uses esbuild, output: dist/index.js
# Start production server
npm start
# Runs: node -r dotenv/config dist/index.js
# Push database schema changes
npm run db:push
# Test Winston logger (after build)
npm run test:loggerserver/
├── index.ts # Server entry point (Express app, WebSocket, Vite dev)
├── routes.ts # ALL API routes (~1100 lines)
├── storage.ts # Database operations (~1200 lines, implements IStorage)
├── simple-storage.ts # Legacy/simple storage (not used in production)
├── db.ts # PostgreSQL connection pool
├── logger.ts # Winston logger configuration
├── vite.ts # Vite dev server integration (development only)
└── __tests__/ # Test files
├── routes.test.ts
└── storage.test.ts
- Files: camelCase (e.g.,
storage.ts,routes.ts) - Functions: camelCase (e.g.,
getSnippets,createUser) - Interfaces: PascalCase with
Iprefix (e.g.,IStorage) - Database operations: Follow CRUD pattern (get, create, update, delete)
// ✅ Good: server/routes.ts
import { storage } from "./storage";
app.get("/api/snippets", authMiddleware, async (req, res) => {
const snippets = await storage.getSnippets({ userId: req.user.id });
res.json(snippets);
});
// ❌ Bad: Direct database queries in routes
import { pool } from "./db";
const result = await pool.query("SELECT * FROM snippets");// ✅ Good: Protect routes with authMiddleware
import { authMiddleware } from "./routes";
app.post("/api/snippets", authMiddleware, async (req, res) => {
// req.user is now available (populated by authMiddleware)
const snippet = await storage.createSnippet({
...req.body,
userId: req.user.id
});
res.json(snippet);
});
// ❌ Bad: No auth on protected routes
app.post("/api/snippets", async (req, res) => {
// Anyone can create snippets!
});// ✅ Good: Use schemas from @shared/schema
import { insertSnippetSchema } from "@shared/schema";
app.post("/api/snippets", authMiddleware, async (req, res) => {
try {
const validated = insertSnippetSchema.parse(req.body);
const snippet = await storage.createSnippet(validated);
res.json(snippet);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ message: "Validation failed", errors: error.errors });
}
throw error;
}
});
// ❌ Bad: No validation
app.post("/api/snippets", async (req, res) => {
const snippet = await storage.createSnippet(req.body); // Trust user input
});// ✅ Good: server/storage.ts pattern
import { db } from "./db";
import { snippets, type Snippet } from "@shared/schema";
import { eq, and, ilike } from "drizzle-orm";
async getSnippets(filters?: { search?: string; userId?: string }): Promise<Snippet[]> {
let query = db.select().from(snippets);
const conditions = [];
if (filters?.userId) conditions.push(eq(snippets.userId, filters.userId));
if (filters?.search) conditions.push(ilike(snippets.title, `%${filters.search}%`));
if (conditions.length > 0) {
query = query.where(and(...conditions));
}
return await query;
}
// ❌ Bad: Raw SQL strings
await pool.query(`SELECT * FROM snippets WHERE user_id = $1`, [userId]);// ✅ Good: Use Winston logger
import logger from "./logger";
logger.info("Server started", { port: 3000 });
logger.error("Database error", { error: err.message });
logger.debug("Request received", { path: req.path });
// ❌ Bad: console.log (not persistent, no log levels)
console.log("Server started");
console.error("Error:", err);// ✅ Good: Structured error responses
app.get("/api/snippets/:id", authMiddleware, async (req, res) => {
try {
const snippet = await storage.getSnippet(Number(req.params.id));
if (!snippet) {
return res.status(404).json({ message: "Snippet not found" });
}
res.json(snippet);
} catch (error: any) {
logger.error("Error fetching snippet", { error: error.message });
res.status(500).json({ message: "Internal server error" });
}
});
// ❌ Bad: Unhandled errors, no status codes
app.get("/api/snippets/:id", async (req, res) => {
const snippet = await storage.getSnippet(Number(req.params.id));
res.json(snippet); // Crashes if snippet is undefined
});// ✅ Good: server/routes.ts authMiddleware pattern
import admin from "firebase-admin";
export const authMiddleware: RequestHandler = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ message: "Unauthorized: No token" });
}
const idToken = authHeader.split(" ")[1];
const decoded = await admin.auth().verifyIdToken(idToken);
const user = await storage.getUser(decoded.uid);
(req as any).user = user;
next();
} catch (err: any) {
res.status(401).json({ message: "Unauthorized: Invalid token" });
}
};
// ❌ Bad: No token verification
app.use((req, res, next) => {
req.user = { id: "fake-user" }; // Trust client-provided data
next();
});// ✅ Good: Use TypeScript property names (camelCase)
const snippet = await storage.getSnippet(1);
console.log(snippet.userId, snippet.createdAt); // Drizzle handles mapping
// ❌ Bad: Using database column names directly
console.log(snippet.userid, snippet.createdat); // TypeScript error!- Entry Point:
server/index.ts- Express app initialization, WebSocket setup, Vite integration - API Routes:
server/routes.ts- ALL API endpoints (auth, snippets, collections, comments) - Storage Layer:
server/storage.ts- Database operations (implements IStorage interface) - Database Connection:
server/db.ts- PostgreSQL pool using@neondatabase/serverless - Logger:
server/logger.ts- Winston configuration (logs to~/logs/codepatchwork.log) - Auth Middleware:
server/routes.ts:authMiddleware- Firebase token verification
The storage.ts file implements this interface:
export interface IStorage {
// Users
getUser(id: string): Promise<User | undefined>;
createUser(user: InsertUser): Promise<User>;
// Snippets
getSnippets(filters?: { search?: string; language?: string; userId?: string }): Promise<Snippet[]>;
getSnippet(id: number): Promise<Snippet | undefined>;
createSnippet(snippet: InsertSnippet): Promise<Snippet>;
updateSnippet(id: number, snippet: InsertSnippet): Promise<Snippet>;
deleteSnippet(id: number): Promise<void>;
toggleSnippetFavorite(id: number): Promise<Snippet>;
// Collections
getCollections(): Promise<Collection[]>;
createCollection(collection: InsertCollection): Promise<Collection>;
deleteCollection(id: number): Promise<void>;
// Collection Items
addSnippetToCollection(item: InsertCollectionItem): Promise<CollectionItem>;
removeSnippetFromCollection(collectionId: number, snippetId: number): Promise<void>;
// Comments
getComments(snippetId: number): Promise<Comment[]>;
createComment(comment: InsertComment): Promise<Comment>;
// Sharing
generateShareId(snippetId: number): Promise<string>;
getSnippetByShareId(shareId: string): Promise<Snippet | undefined>;
}Pattern: Routes call storage methods, storage methods use Drizzle ORM.
# Find all API routes
rg -n "app\.(get|post|put|patch|delete)" server/routes.ts
# Find storage methods
rg -n "async (get|create|update|delete)" server/storage.ts
# Find auth middleware usage
rg -n "authMiddleware" server/routes.ts
# Find database queries
rg -n "db\.(select|insert|update|delete)" server/storage.ts
# Find all logger calls
rg -n "logger\.(info|error|warn|debug)" server/
# Check TypeScript errors (server only)
npm run check 2>&1 | grep "server/"
# Test database connection
npm run db:pushIssue: PostgreSQL columns are lowercase (e.g., userid), but TypeScript uses camelCase (e.g., userId).
// ✅ Good: Drizzle schema maps column names
export const snippets = pgTable("snippets", {
userId: text("userid"), // TS: userId, DB: userid
createdAt: timestamp("createdat"), // TS: createdAt, DB: createdat
});
// In code, always use TypeScript property names:
snippet.userId // ✅ Correct
snippet.userid // ❌ TypeScript error
// Drizzle handles the mapping automatically in queries// After authMiddleware, req.user is available:
app.post("/api/snippets", authMiddleware, async (req, res) => {
const user = (req as any).user; // User from storage
// Use user.id for userId fields
});// server/index.ts initializes Firebase Admin
import admin from "firebase-admin";
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
// ... other credentials
})
});
// Don't re-initialize elsewhere!# ✅ Good: Server-side env vars (no VITE_ prefix)
DATABASE_URL=postgres://...
FIREBASE_PROJECT_ID=...
NODE_ENV=production
# ❌ Bad: Using VITE_ prefix on server
VITE_DATABASE_URL=... # Won't be loaded by Node.js// Winston is configured to write to:
// ~/logs/codepatchwork.log
// Check logs:
tail -f ~/logs/codepatchwork.log
// Test logger after build:
npm run build
npm run test:logger// ✅ Good: Use the full storage implementation
import { storage } from "./storage";
// ❌ Bad: simpleStorage is legacy/incomplete
import { simpleStorage } from "./simple-storage";Before creating a pull request:
# 1. Typecheck passes
npm run check
# 2. Build succeeds
npm run build
# 3. Test database connection
npm run db:push
# 4. Check for hardcoded secrets
grep -r "AIza" server/ # Should only find in comments/examples
grep -r "firebase" server/*.ts | grep -v "import" | grep -v "//"
# 5. Verify logger works (after build)
npm run test:logger
cat ~/logs/codepatchwork.log// GET /api/snippets - List snippets
app.get("/api/snippets", authMiddleware, async (req, res) => {
const filters = {
search: req.query.search as string | undefined,
language: req.query.language as string | undefined,
userId: (req as any).user.id
};
const snippets = await storage.getSnippets(filters);
res.json(snippets);
});
// POST /api/snippets - Create snippet
app.post("/api/snippets", authMiddleware, async (req, res) => {
const validated = insertSnippetSchema.parse(req.body);
const snippet = await storage.createSnippet({
...validated,
userId: (req as any).user.id
});
res.status(201).json(snippet);
});
// PATCH /api/snippets/:id - Update snippet
app.patch("/api/snippets/:id", authMiddleware, async (req, res) => {
const validated = insertSnippetSchema.partial().parse(req.body);
const snippet = await storage.updateSnippet(Number(req.params.id), validated);
res.json(snippet);
});
// DELETE /api/snippets/:id - Delete snippet
app.delete("/api/snippets/:id", authMiddleware, async (req, res) => {
await storage.deleteSnippet(Number(req.params.id));
res.status(204).send();
});# 1. Edit shared/schema.ts (add/modify tables)
# 2. Push changes to database
npm run db:push
# 3. Drizzle Kit will generate and apply migrations
# Output: migrations/ directory
# 4. Commit schema changes and migrations
git add shared/schema.ts migrations/
git commit -m "Add new table: xyz"Note: Tests exist in server/__tests__/ but no test runner is configured in package.json yet.
Example test structure:
server/
├── routes.ts
├── storage.ts
└── __tests__/
├── routes.test.ts
└── storage.test.ts
When test runner is added, run:
# (Future) Run server tests
npm run test:server # Not yet implementedFor a complete example of a storage method, see:
server/storage.ts:getSnippets
- Drizzle query builder
- Dynamic filtering with
and()andor() - Array handling for tags
- ILIKE for case-insensitive search
- Ordering and limiting
- Type safety with TypeScript
This is the gold standard for storage layer implementation.
// 1. Initialize Firebase Admin
admin.initializeApp({ /* ... */ });
// 2. Create Express app
const app = express();
// 3. Middleware
app.use(helmet());
app.use(express.json());
app.use(session({ /* ... */ }));
// 4. Register routes
await registerRoutes(app);
// 5. Vite dev server (development only)
if (NODE_ENV === "development") {
await setupVite(app, server);
}
// 6. Static files (production)
if (NODE_ENV === "production") {
app.use(express.static("dist/public"));
}
// 7. Start server
server.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
});This structure is critical — don't rearrange middleware order!