We offer you a grace period for 3 days to use VDS to check your projects.
A mini Express application that:
Suitable for quick start, testing, and simple landing pages/mini-services.
mini-blog/ +-- package.json +-- app.js +-- db.js +-- .env # Optional for localhost +-- public/ | +-- style.css +-- views/ +-- layout-start.html +-- layout-end.html +-- index.html # Post list +-- view.html # View post +-- new.html # Add form
{ "name": "mini-blog", "version": "1.0.0", "description": "Simple CRUD blog on Express + SQLite for shared hosting (ISPmanager).", "main": "app.js", "scripts": { "start": "node app.js", "start:prod": "NODE_ENV=production node app.js", "initdb": "node db.js --init" }, "engines": { "node": ">=18.x" }, "dependencies": { "dotenv": "^16.4.5", "express": "^4.19.2", "express-rate-limit": "^7.4.0", "express-validator": "^7.2.0", "morgan": "^1.10.0", "sqlite3": "^5.1.7", "helmet": "^7.1.0" } }
If compiling sqlite3 on shared hosting doesn’t work, try a different sqlite3 minor version, or switch to a prebuilt Node.js release (typically LTS) in ISPmanager.
// db.js const fs = require('fs'); const path = require('path'); const sqlite3 = require('sqlite3').verbose(); const DB_PATH = path.join(__dirname, 'data.sqlite'); function connect() { return new sqlite3.Database(DB_PATH); } function init() { const db = connect(); db.serialize(() => { db.run(` CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, created_at INTEGER NOT NULL ) `); db.run(`CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at DESC)`); }); db.close(); console.log('Database initialized:', DB_PATH); } function seed() { const db = connect(); const now = Date.now(); db.serialize(() => { db.run( `INSERT INTO posts (title, content, created_at) VALUES (?, ?, ?)`, ['Welcome', 'This is the first post in the mini-blog using Express + SQLite.', now] ); }); db.close(); console.log('Seed inserted.'); } if (process.argv.includes('--init')) { init(); seed(); } module.exports = { connect, DB_PATH };
// app.js require('dotenv').config(); const path = require('path'); const express = require('express'); const helmet = require('helmet'); const morgan = require('morgan'); const rateLimit = require('express-rate-limit'); const { body, validationResult } = require('express-validator'); const sqlite3 = require('sqlite3').verbose(); const { connect } = require('./db'); const app = express(); const PORT = process.env.PORT || 3000; const isProd = process.env.NODE_ENV === 'production'; // Security, logging, parsing app.use(helmet()); app.use(morgan(isProd ? 'combined' : 'dev')); app.use(express.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname, 'public'))); // Simple "templates": include the common layout via files const layoutStart = path.join(__dirname, 'views', 'layout-start.html'); const layoutEnd = path.join(__dirname, 'views', 'layout-end.html'); const views = { index: path.join(__dirname, 'views', 'index.html'), view: path.join(__dirname, 'views', 'view.html'), form: path.join(__dirname, 'views', 'new.html'), }; // Helpers const fs = require('fs'); function readView(filePath) { return fs.readFileSync(filePath, 'utf8'); } function page(html) { return readView(layoutStart) + html + readView(layoutEnd); } function escapeHtml(str = '') { return String(str) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function nl2br(str='') { return escapeHtml(str).replace(/\n/g, ''); } // Health app.get('/health', (req, res) => res.json({ status: 'ok', uptime: process.uptime() })); // Home: posts list (10 per page) app.get('/', (req, res) => { const db = connect(); const pageNum = Math.max(1, parseInt(req.query.page || '1', 10)); const limit = 10; const offset = (pageNum - 1) * limit; db.all( `SELECT id, title, created_at FROM posts ORDER BY created_at DESC LIMIT ? OFFSET ?`, [limit, offset], (err, rows) => { if (err) { db.close(); return res.status(500).send(page('Failed to load')); } db.get(`SELECT COUNT(*) AS cnt FROM posts`, [], (err2, rowCnt) => { db.close(); const total = rowCnt ? rowCnt.cnt : 0; const totalPages = Math.max(1, Math.ceil(total / limit)); const html = readView(views.index) .replace('{{POSTS}}', rows.map(r => ` ${escapeHtml(r.title)} ${new Date(r.created_at).toLocaleString()} `).join('') || 'No posts available.') .replace('{{PAGINATION}}', ` ${pageNum > 1 ? `Back` : 'Back'} Стр. ${pageNum}/${totalPages} ${pageNum < totalPages ? `Next` : 'Next'} `); res.send(page(html)); }); } ); }); // View post app.get('/post/:id', (req, res) => { const db = connect(); db.get(`SELECT * FROM posts WHERE id = ?`, [req.params.id], (err, row) => { db.close(); if (err || !row) { return res.status(404).send(page('Post not found')); } const html = readView(views.view) .replace('{{TITLE}}', escapeHtml(row.title)) .replace('{{DATE}}', new Date(row.created_at).toLocaleString()) .replace('{{CONTENT}}', nl2br(row.content)) .replace('{{DELETE_LINK}}', `/post/${row.id}/delete`); res.send(page(html)); }); }); // Create post form app.get('/new', (req, res) => { const html = readView(views.form).replace('{{ERRORS}}', ''); res.send(page(html)); }); // POST rate limiter (anti-spam) const postLimiter = rateLimit({ windowMs: 60 * 1000, max: 10, standardHeaders: true, legacyHeaders: false, }); // Create post app.post( '/new', postLimiter, body('title').trim().isLength({ min: 2, max: 200 }).withMessage('Enter a title'), body('content').trim().isLength({ min: 5, max: 20000 }).withMessage('Enter content'), (req, res) => { const errors = validationResult(req); const { title = '', content = '' } = req.body; if (!errors.isEmpty()) { const msg = errors.array().map(e => e.msg).join(', '); const html = readView(views.form).replace('{{ERRORS}}', `${escapeHtml(msg)}`); return res.status(400).send(page(html)); } const db = connect(); db.run( `INSERT INTO posts (title, content, created_at) VALUES (?, ?, ?)`, [title, content, Date.now()], function (err) { db.close(); if (err) return res.status(500).send(page('Failed to save')); res.redirect(`/post/${this.lastID}`); } ); } ); // Delete post (example uses a GET link; in production use POST/DELETE with a token) app.get('/post/:id/delete', (req, res) => { const db = connect(); db.run(`DELETE FROM posts WHERE id = ?`, [req.params.id], function (err) { db.close(); if (err) return res.status(500).send(page('Failed to delete')); res.redirect('/'); }); }); // 404 и 500 app.use((req, res) => res.status(404).send(page('404 Not Found'))); app.use((err, req, res, next) => { console.error('Unhandled error:', err); res.status(500).send(page('500 Internal Server Error')); }); app.listen(PORT, () => { console.log(`Mini-blog running on port ${PORT}`); });
No posts available.
${escapeHtml(msg)}
views/layout-start.html
<!doctype html> <html lang="ru"> <head> <meta charset="utf-8" /> <title>Mini-blog on Express + SQLite</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link href="/style.css" rel="stylesheet" /> </head> <body> <header class="container"> <a class="logo" href="/">MiniBlog</a> <nav><a class="btn" href="/new">New post</a></nav> </header> <main class="container">
views/layout-end.html
</main> <footer class="container footer"> <p>Mini-blog on Node.js (Express) + SQLite.. <a href="/health">/health</a></p> </footer> </body> </html> views/index.html <section> <h1>Blog posts</h1> {{POSTS}} {{PAGINATION}} </section>
views/view.html
<article class="post"> <h1>{{TITLE}}</h1> <div class="meta">{{DATE}}</div> <div class="content">{{CONTENT}}</div> <p><a class="danger" href="{{DELETE_LINK}}" onclick="return confirm('Delete post?');">Remove</a></p> <p><a href="/">На главную</a></p> </article>
views/new.html
<section class="card"> <h1>New post</h1> {{ERRORS}} <form method="post" action="/new" novalidate> <label>Title <input type="text" name="title" required minlength="2" maxlength="200" placeholder="Example: First post." /> </label> <label>Content <textarea name="content" required minlength="5" maxlength="20000" rows="10" placeholder="Post content..."></textarea> </label> <button type="submit">Submit</button> </form> </section>
:root{ --bg:#0f172a; --card:#111827; --text:#e2e8f0; --muted:#94a3b8; --primary:#22c55e; --primary-hover:#16a34a; --danger:#ef4444; } *{box-sizing:border-box} html,body{margin:0;font:16px/1.6 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;background:var(--bg);color:var(--text)} .container{max-width:900px;margin:0 auto;padding:20px} header.container{display:flex;justify-content:space-between;align-items:center} .logo{font-weight:800;text-decoration:none;color:var(--text)} .btn{background:var(--primary);color:#052e16;padding:10px 14px;border-radius:10px;text-decoration:none;font-weight:700} .btn:hover{background:var(--primary-hover)} .card{background:var(--card);border-radius:16px;padding:20px;box-shadow:0 10px 30px rgba(0,0,0,.25)} .post{background:var(--card);border-radius:16px;padding:18px;margin:14px 0} .meta{color:var(--muted);font-size:14px;margin-bottom:8px} .content{white-space:normal} label{display:grid;gap:6px;margin:10px 0} input,textarea{width:100%;padding:10px 12px;border:1px solid #1f2937;border-radius:10px;background:#0b1220;color:var(--text)} button{padding:12px 16px;border:0;border-radius:10px;background:var(--primary);color:#052e16;font-weight:700;cursor:pointer} button:hover{background:var(--primary-hover)} .pagination{display:flex;gap:12px;align-items:center;margin-top:16px} .pagination a{color:var(--text)} .muted{color:var(--muted)} a.danger{color:var(--danger);text-decoration:none} .footer{color:var(--muted)} h1,h2{margin-top:0}
PORT=3000 NODE_ENV=development
Locally
npm install npm run initdb # creates data.sqlite and the first post npm start # http://localhost:3000
All set: homepage - list of posts, /new - add form, /post/:id - post view.
The app listens on process.env.PORT. DB initialized (npm run initdb). HTTPS enabled, /post/:id/delete is protected by authentication (production). Backup the project directory (including data.sqlite)