← Blog
PolsiaPlaybook · May 21, 2026

Free Code Templates for Startups: 10 Copy-Paste Boilerplates That Save Days

10 battle-tested code templates for indie builders: auth, Stripe webhooks, JWT, Postgres queries, Express setup — copy, swap vars, ship. Free access below.

Free Code Templates for Startups: 10 Copy-Paste Boilerplates That Save Days

Every startup begins with the same 500 lines of plumbing. Auth, database connections, API patterns, error handling, deployment configs — code that has nothing to do with your product idea but has to exist before you can build it.

These 10 templates are the ones I wish someone had handed me. Each is production-ready, opinionated, and built for the stack that ships the most MVPs: Node.js, Express, and PostgreSQL.

Copy them. Adapt them. Ship faster.


Template 1: Express.js App Entry Point

The canonical Express setup that stays under 100 lines and wires everything correctly — middleware order matters here.

const express = require('express');
const cookieParser = require('cookie-parser');
const path = require('path');

const app = express();
const PORT = process.env.PORT || 3000;

// Stripe webhook MUST come before express.json (needs raw body)
app.post('/api/webhook', express.raw({ type: 'application/json' }), require('./routes/webhook'));

// Core middleware
app.use(cookieParser());
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));

// Health check — required by most hosting platforms
app.get('/health', (req, res) => res.json({ status: 'ok', ts: Date.now() }));

// Routes
app.use('/api/auth',    require('./routes/auth'));
app.use('/api/users',   require('./routes/users'));
app.use('/api/content', require('./routes/content'));

// Global error handler
app.use((err, req, res, next) => {
  console.error('Unhandled error:', err.message);
  res.status(500).json({ error: 'Internal server error' });
});

app.listen(PORT, () => console.log(`Server on port ${PORT}`));

Why this order matters: Raw body parsing for webhooks must happen before express.json() consumes the stream. Skip this and Stripe signature verification breaks silently.

👉 See the full Code Templates library →


Template 2: PostgreSQL Connection Pool

One pool, shared across the app. Never create a new Pool per request.

// db/index.js
const { Pool } = require('pg');

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  // Neon, Supabase, Railway: add SSL
  ssl: process.env.DATABASE_URL && process.env.DATABASE_URL.includes('neon')
    ? { rejectUnauthorized: false }
    : false,
  max: 10,                // max connections in pool
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 5000,
});

pool.on('error', (err) => {
  console.error('Unexpected DB pool error:', err.message);
});

module.exports = pool;
// db/users.js — example query module
const pool = require('./index');

async function getUserById(id) {
  const { rows } = await pool.query(
    'SELECT id, email, created_at FROM users WHERE id = $1',
    [id]
  );
  return rows[0] || null;
}

async function getUserByEmail(email) {
  const { rows } = await pool.query(
    'SELECT * FROM users WHERE email = $1',
    [email]
  );
  return rows[0] || null;
}

async function createUser({ email, passwordHash }) {
  const { rows } = await pool.query(
    'INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id, email, created_at',
    [email, passwordHash]
  );
  return rows[0];
}

module.exports = { getUserById, getUserByEmail, createUser };

The db/ layer is a hard boundary — no raw pool.query calls outside this directory.


Template 3: JWT Auth Middleware

Stateless authentication that works across serverless and traditional deployments.

// middleware/auth.js
const jwt = require('jsonwebtoken');

const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) throw new Error('JWT_SECRET env var is required');

function requireAuth(req, res, next) {
  const header = req.headers.authorization;
  const token = header && header.startsWith('Bearer ')
    ? header.slice(7)
    : req.cookies && req.cookies.token;

  if (!token) return res.status(401).json({ error: 'Authentication required' });

  try {
    req.user = jwt.verify(token, JWT_SECRET);
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}

// Use on routes that can work with or without auth
function optionalAuth(req, res, next) {
  const header = req.headers.authorization;
  const token = header && header.startsWith('Bearer ')
    ? header.slice(7)
    : req.cookies && req.cookies.token;

  if (token) {
    try { req.user = jwt.verify(token, JWT_SECRET); } catch (_) {}
  }
  next();
}

module.exports = { requireAuth, optionalAuth };
// Usage in a route
const { requireAuth } = require('../middleware/auth');

router.get('/me', requireAuth, async (req, res) => {
  const user = await getUserById(req.user.id);
  res.json(user);
});

Security note: Set httpOnly: true and secure: true on the cookie in production. The middleware reads from both header and cookie so you can support API clients and browser sessions with the same code.


Template 4: Auth Route — Register + Login

// routes/auth.js
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { getUserByEmail, createUser } = require('../db/users');

const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET;
const COOKIE_OPTS = {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
};

router.post('/register', async (req, res) => {
  const { email, password } = req.body;
  if (!email || !password) return res.status(400).json({ error: 'Email and password required' });
  if (password.length < 8) return res.status(400).json({ error: 'Password must be at least 8 characters' });

  try {
    const existing = await getUserByEmail(email.toLowerCase().trim());
    if (existing) return res.status(409).json({ error: 'Email already registered' });

    const passwordHash = await bcrypt.hash(password, 12);
    const user = await createUser({ email: email.toLowerCase().trim(), passwordHash });

    const token = jwt.sign({ id: user.id, email: user.email }, JWT_SECRET, { expiresIn: '7d' });
    res.cookie('token', token, COOKIE_OPTS);
    res.status(201).json({ user: { id: user.id, email: user.email } });
  } catch (err) {
    console.error('Register error:', err.message);
    res.status(500).json({ error: 'Registration failed' });
  }
});

router.post('/login', async (req, res) => {
  const { email, password } = req.body;
  if (!email || !password) return res.status(400).json({ error: 'Email and password required' });

  try {
    const user = await getUserByEmail(email.toLowerCase().trim());
    if (!user) return res.status(401).json({ error: 'Invalid email or password' });

    const valid = await bcrypt.compare(password, user.password_hash);
    if (!valid) return res.status(401).json({ error: 'Invalid email or password' });

    const token = jwt.sign({ id: user.id, email: user.email }, JWT_SECRET, { expiresIn: '7d' });
    res.cookie('token', token, COOKIE_OPTS);
    res.json({ user: { id: user.id, email: user.email } });
  } catch (err) {
    console.error('Login error:', err.message);
    res.status(500).json({ error: 'Login failed' });
  }
});

router.post('/logout', (req, res) => {
  res.clearCookie('token');
  res.json({ ok: true });
});

module.exports = router;

Same error message for invalid email and wrong password — deliberately. Never confirm whether an email is registered.


Template 5: CRUD API Endpoint Pattern

The pattern for every data entity in your app. Consistent, predictable, easy to extend.

// routes/items.js
const express = require('express');
const { requireAuth } = require('../middleware/auth');
const { listItems, getItemById, createItem, updateItem, deleteItem } = require('../db/items');

const router = express.Router();

// GET /api/items — list all (paginated)
router.get('/', requireAuth, async (req, res) => {
  const page = Math.max(1, parseInt(req.query.page) || 1);
  const limit = Math.min(100, parseInt(req.query.limit) || 20);
  const offset = (page - 1) * limit;

  try {
    const items = await listItems({ userId: req.user.id, limit, offset });
    res.json({ items, page, limit });
  } catch (err) {
    console.error('List items error:', err.message);
    res.status(500).json({ error: 'Failed to fetch items' });
  }
});

// GET /api/items/:id — get one
router.get('/:id', requireAuth, async (req, res) => {
  try {
    const item = await getItemById(req.params.id);
    if (!item || item.user_id !== req.user.id) return res.status(404).json({ error: 'Not found' });
    res.json(item);
  } catch (err) {
    res.status(500).json({ error: 'Failed to fetch item' });
  }
});

// POST /api/items — create
router.post('/', requireAuth, async (req, res) => {
  const { name, description } = req.body;
  if (!name) return res.status(400).json({ error: 'Name is required' });

  try {
    const item = await createItem({ userId: req.user.id, name, description });
    res.status(201).json(item);
  } catch (err) {
    console.error('Create item error:', err.message);
    res.status(500).json({ error: 'Failed to create item' });
  }
});

// PUT /api/items/:id — update
router.put('/:id', requireAuth, async (req, res) => {
  try {
    const existing = await getItemById(req.params.id);
    if (!existing || existing.user_id !== req.user.id) return res.status(404).json({ error: 'Not found' });

    const item = await updateItem(req.params.id, req.body);
    res.json(item);
  } catch (err) {
    res.status(500).json({ error: 'Failed to update item' });
  }
});

// DELETE /api/items/:id — delete
router.delete('/:id', requireAuth, async (req, res) => {
  try {
    const existing = await getItemById(req.params.id);
    if (!existing || existing.user_id !== req.user.id) return res.status(404).json({ error: 'Not found' });

    await deleteItem(req.params.id);
    res.json({ ok: true });
  } catch (err) {
    res.status(500).json({ error: 'Failed to delete item' });
  }
});

module.exports = router;

👉 Browse the full code templates library →


Template 6: Database Migration Runner

Roll-forward-only migrations that run once and track state in the DB. No ORM required.

// migrate.js
const pool = require('./db/index');
const fs = require('fs');
const path = require('path');

async function migrate() {
  const client = await pool.connect();
  try {
    // Create migrations tracking table if needed
    await client.query(`
      CREATE TABLE IF NOT EXISTS _migrations (
        id SERIAL PRIMARY KEY,
        name VARCHAR(255) NOT NULL UNIQUE,
        run_at TIMESTAMPTZ DEFAULT NOW()
      )
    `);

    // Load all migration files sorted by filename
    const dir = path.join(__dirname, 'migrations');
    const files = fs.readdirSync(dir)
      .filter(f => f.endsWith('.js'))
      .sort();

    for (const file of files) {
      const migration = require(path.join(dir, file));
      const { rows } = await client.query(
        'SELECT id FROM _migrations WHERE name = $1', [migration.name]
      );
      if (rows.length > 0) continue; // Already ran

      console.log(`Running migration: ${migration.name}`);
      await client.query('BEGIN');
      try {
        await migration.up(client);
        await client.query('INSERT INTO _migrations (name) VALUES ($1)', [migration.name]);
        await client.query('COMMIT');
        console.log(`✓ ${migration.name}`);
      } catch (err) {
        await client.query('ROLLBACK');
        console.error(`✗ ${migration.name}: ${err.message}`);
        process.exit(1);
      }
    }
    console.log('Migrations complete.');
  } finally {
    client.release();
    await pool.end();
  }
}

migrate().catch(err => { console.error(err); process.exit(1); });

Pair with package.json: "migrate": "node migrate.js" and "start": "npm run migrate && node server.js".


Template 7: Stripe Webhook Handler

The exact pattern for handling Stripe webhooks correctly — signature verification first, idempotency second.

// routes/webhook.js
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const { updateUserSubscription } = require('../db/users');

const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;

async function stripeWebhook(req, res) {
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    // req.body must be the raw Buffer — wire express.raw() BEFORE express.json()
    event = stripe.webhooks.constructEvent(req.body, sig, WEBHOOK_SECRET);
  } catch (err) {
    console.error('Webhook signature failed:', err.message);
    return res.status(400).json({ error: 'Invalid signature' });
  }

  try {
    switch (event.type) {
      case 'checkout.session.completed': {
        const session = event.data.object;
        const customerId = session.customer;
        const email = session.customer_details?.email;
        if (email) await updateUserSubscription(email, { status: 'active', stripeCustomerId: customerId });
        break;
      }
      case 'customer.subscription.deleted': {
        const sub = event.data.object;
        await updateUserSubscription(null, { status: 'cancelled', stripeCustomerId: sub.customer });
        break;
      }
      case 'invoice.payment_failed': {
        const invoice = event.data.object;
        console.warn('Payment failed for customer:', invoice.customer);
        // Send dunning email here
        break;
      }
    }
    res.json({ received: true });
  } catch (err) {
    console.error('Webhook handler error:', err.message);
    res.status(500).json({ error: 'Handler failed' });
  }
}

module.exports = { stripeWebhook };

Template 8: Environment Variable Validation at Startup

Crash loudly on startup if required env vars are missing. Far better than mysterious runtime failures.

// config/env.js
const REQUIRED_VARS = [
  'DATABASE_URL',
  'JWT_SECRET',
  'STRIPE_SECRET_KEY',
  'STRIPE_WEBHOOK_SECRET',
];

const missing = REQUIRED_VARS.filter(key => !process.env[key]);
if (missing.length > 0) {
  console.error('Missing required environment variables:');
  missing.forEach(key => console.error(`  - ${key}`));
  process.exit(1);
}

module.exports = {
  DATABASE_URL: process.env.DATABASE_URL,
  JWT_SECRET: process.env.JWT_SECRET,
  STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
  STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
  NODE_ENV: process.env.NODE_ENV || 'development',
  PORT: parseInt(process.env.PORT) || 3000,
};

Require this at the top of server.js before anything else. A startup crash with a clear message beats a 3am production incident where half the app works and half silently uses undefined.


Template 9: Rate Limiting Middleware

Simple in-memory rate limiting. Good enough for 99% of MVPs without Redis.

// middleware/rateLimit.js

// In-memory store — resets on restart, fine for most MVPs
const requestCounts = new Map();

function rateLimit({ windowMs = 60_000, max = 20, message = 'Too many requests' } = {}) {
  return (req, res, next) => {
    const key = req.ip;
    const now = Date.now();
    const record = requestCounts.get(key);

    if (!record || now - record.start > windowMs) {
      requestCounts.set(key, { count: 1, start: now });
      return next();
    }

    if (record.count >= max) {
      return res.status(429).json({ error: message });
    }

    record.count++;
    next();
  };
}

// Cleanup old entries every 5 minutes — prevents unbounded memory growth
setInterval(() => {
  const cutoff = Date.now() - 5 * 60_000;
  for (const [key, record] of requestCounts) {
    if (record.start < cutoff) requestCounts.delete(key);
  }
}, 5 * 60_000);

module.exports = { rateLimit };
// Usage — tight limit on auth endpoints
const { rateLimit } = require('../middleware/rateLimit');

router.post('/login', rateLimit({ windowMs: 60_000, max: 5, message: 'Too many login attempts' }), loginHandler);

Template 10: render.yaml Deploy Config

One file that defines your entire Render deployment. Commit it and never touch the dashboard.

services:
  - type: web
    name: my-app
    env: node
    region: oregon
    plan: free
    buildCommand: npm install
    startCommand: npm run migrate && node server.js
    healthCheckPath: /health
    envVars:
      - key: NODE_ENV
        value: production
      - key: DATABASE_URL
        fromDatabase:
          name: my-app-db
          property: connectionString
      - key: JWT_SECRET
        generateValue: true
      - key: STRIPE_SECRET_KEY
        sync: false
      - key: STRIPE_WEBHOOK_SECRET
        sync: false

databases:
  - name: my-app-db
    databaseName: myapp
    user: myapp
    region: oregon
    plan: free

The critical detail: startCommand: npm run migrate && node server.js means migrations run on every deploy. Your migration runner must be idempotent (CREATE TABLE IF NOT EXISTS, ON CONFLICT DO NOTHING) — which the template above handles.


Putting It All Together

These 10 templates give you the skeleton of any Node.js SaaS:

1. Entry file — wires everything, stays small 2. DB pool — shared, configured once 3. Auth middleware — JWT, header + cookie 4. Auth routes — register, login, logout 5. CRUD pattern — consistent across all entities 6. Migration runner — forward-only, tracked in DB 7. Stripe webhook — signature-verified, idempotent 8. Env validation — crash early, crash clearly 9. Rate limiting — no Redis required 10. Render config — deploy from config, not clicks

The startup boilerplate problem is solved. The next 10,000 lines are your actual product.


Get More Templates

These are the free samples from the PolsiaPlaybook library. The full collection has 30+ templates — including email sending, file uploads, admin dashboards, subscription gating, and full-stack patterns.

Browse all free code templates →

See the full Code Templates library →

Or read the companion article:

AI Prompts That Actually Ship Products: 12 Templates →

$12/month gets you the complete library. Cancel any time.

Free toolkit

Get all 9 free tools — instantly

Prompts, templates & playbooks. No signup required to browse.

No spam. Unsubscribe any time.

Get the full prompt library

30+ AI prompts, code templates, and playbooks for indie builders. $12/mo, cancel any time.

Get Full Access — $12/mo Browse free tools first →
More resources
AI Prompts Library → Code Templates → Playbooks → All articles →