Quick Start

Get a working SpaceNode application in under 2 minutes.

1. Install

# Create a new project
mkdir my-app && cd my-app
npm init -y

# Install SpaceNode
npm install SpaceNode

2. Create Entry Point

Create app.js in your project root:

import { createApp } from 'SpaceNode'

const app = await createApp()
app.listen(3000)
Tip — Make sure your package.json has "type": "module" for ESM imports.

3. Create Your First Module

Create the folder modules/hello/ and add two files:

modules/hello/module.js

export default {
  name: 'hello',
  prefix: '/hello',
  routes: [
    ['GET', '/',      'index'],
    ['GET', '/:name', 'greet'],
  ],
}

modules/hello/hello.controller.js

export function index({ send }) {
  send({ message: 'Hello from SpaceNode!' })
}

export function greet({ params, send }) {
  send({ message: `Hello, ${params.name}!` })
}

4. Run

node app.js

You'll see:

  ═══════════════════════════════════════
           ⚡ SpaceNode v1.0.0          
  ═══════════════════════════════════════
    Port:     3000                      
    Modules:  1                         
    Routes:   2                         
    Services: 0                         
  ═══════════════════════════════════════
  GET     /hello
  GET     /hello/:name

5. Test

curl http://localhost:3000/hello
→ {"message":"Hello from SpaceNode!"}

curl http://localhost:3000/hello/World
→ {"message":"Hello, World!"}
That's it! No route registration, no middleware setup, no wiring. Just folders and files.

Three Ways to Use SpaceNode

SpaceNode covers every common web scenario out of the box. Pick the approach that matches your project — or combine them freely in a single app.

Mode What It Is Best For
Static Site Serve plain HTML/CSS/JS with zero code Landing pages, portfolios, SPAs (React / Vue / Svelte)
REST API JSON endpoints with modules, validation, auth Mobile backends, microservices, SPA backends
SSR Site Server-rendered HTML with a built-in template engine SEO-critical pages, dashboards, full-stack apps

1. Static Site

The simplest option — serve static files from a folder. No modules, no controllers, just one line of config. SpaceNode auto-detects 30+ MIME types, adds ETag caching, and protects against directory traversal.

When to use: landing pages, documentation sites, portfolios, any pre-built SPA (React, Vue, Svelte, Astro, etc.).

Minimal Example

// app.js
import { createApp } from 'SpaceNode'

const app = await createApp({
  static: './public',   // folder with your HTML/CSS/JS
  spa:    false,        // multi-page mode (true → SPA fallback to index.html)
})
app.listen(3000)

Project Structure

my-site/
├── app.js
├── package.json
└── public/
    ├── index.html
    ├── about.html
    ├── style.css
    ├── 404.html         ← custom error page (auto-detected)
    └── images/
        └── logo.png
Clean URLs/about automatically resolves to about.html
SPA ModeSet spa: true (default) — unknown paths fall back to index.html
ETag & 304Built-in caching: browsers skip re-downloading unchanged files
Large filesFiles over 256 KB are streamed, not buffered into memory
SecurityDirectory traversal attacks are blocked automatically
Tip — Add pipe: ['compress', 'security'] for production to enable gzip and security headers.

2. REST API

The module-based pattern you saw in the quick start above. Each module lives in its own folder with a module.js (routes), a controller (handlers), and optionally services, DTOs, and guards. SpaceNode auto-discovers modules and wires everything together.

When to use: any JSON backend — mobile apps, SPA backends, microservices, third-party integrations.

Feature Highlights

Auto Module LoadingDrop a folder into modules/ — it's registered automatically
DTO ValidationDeclare expected fields & types in a DTO; invalid requests are rejected before your code runs
Auth & GuardsdefineAuth() for JWT/session auth, defineGuard() for role checks
CORSAdd 'cors' to pipe — preflight and headers are handled for you
OpenAPIPass openapi: { title, version } and a Swagger spec is generated from your routes
Event Busrequest.emit('order.created', data) for decoupled side-effects
Services & DIExport a service.js — it's auto-injected into controllers as the second argument

Typical REST App Structure

my-api/
├── app.js
├── package.json
└── modules/
    ├── auth/
    │   ├── module.js
    │   ├── auth.controller.js
    │   ├── auth.service.js
    │   └── auth.dto.js
    └── products/
        ├── module.js
        ├── products.controller.js
        ├── products.service.js
        └── products.dto.js

Entry Point with Auth & CORS

// app.js
import { createApp, defineAuth } from 'SpaceNode'

defineAuth(async (req) => {
  const token = req.headers['authorization']?.replace('Bearer ', '')
  if (!token) return null
  return verifyJWT(token)  // return user object or null
})

const app = await createApp({
  pipe: ['cors', 'logger'],
  openapi: { title: 'My API', version: '1.0.0' },
})
app.listen(3000)

Module Example

// modules/products/module.js
export default {
  name: 'products',
  prefix: '/products',
  routes: [
    ['GET',    '/',    'list'],
    ['GET',    '/:id', 'getOne'],
    ['POST',   '/',    'create', ['auth', 'dto:createDto']],
    ['DELETE', '/:id', 'remove', ['auth', 'role:admin']],
  ],
}
// modules/products/products.controller.js
export function list(req, { productService }) {
  const items = await productService.findAll()
  req.send(items)
}

export function create(req, { productService }) {
  const product = await productService.create(req.body)
  req.send(product, 201)
}
Tip — Routes with ['auth', 'dto:createDto'] pipes run authentication and DTO validation automatically before your handler is called.

3. SSR Site (Server-Side Rendering)

SpaceNode includes a full template engine compiled ahead-of-time. Write .html templates with [= variable] syntax, layouts, partials, pipe filters — and render pages on the server with data from your database. No React, no build step, just HTML and JavaScript.

When to use: SEO-critical websites (content, blogs, e-commerce), admin dashboards, multi-page apps with forms, any project where you want server-rendered HTML without a frontend framework.

Feature Highlights

Template Syntax[= expr] output, [# if/each/block] logic, [> include] partials
Auto EscapingAll output is HTML-escaped by default — XSS protection built in
LayoutsOne-level layout wrapping every page; [= body] injects page content
Pipe Filters[= price | currency] — 10+ built-in helpers, add custom ones via addHelper()
AOT CompilationTemplates → AST → JS functions — near-zero render overhead
CSRFAdd 'csrf' pipe and [= csrfField] is auto-available in forms
Flash Messagesrequest.flash('success', msg) — displayed once on next page
LRU CacheCompiled templates cached in memory (default 500 entries)

Enable the Template Engine

// app.js
import { createApp } from 'SpaceNode'

const app = await createApp({
  views:   './views',     // templates directory
  static:  './public',    // CSS, JS, images
  baseUrl: import.meta.url,
})
app.listen(3000)

SSR Project Structure

my-site/
├── app.js
├── package.json
├── modules/
│   ├── auth/
│   │   ├── module.js
│   │   └── auth.controller.js
│   └── profile/
│       ├── module.js
│       └── profile.controller.js
├── views/
│   ├── settings.js        ← layout & globals config
│   ├── layout.html        ← site-wide wrapper
│   ├── home.html
│   ├── login.html
│   ├── nav.html           ← partial (include)
│   └── footer.html
└── public/
    └── css/
        └── style.css

Templates

<!-- views/layout.html -->
<!DOCTYPE html>
<html>
<head>
  <title>[= title][= siteName]</title>
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  [> nav]
  <main>[= body]</main>
  [> footer]
</body>
</html>
<!-- views/home.html -->
<h1>Welcome, [= user.name]!</h1>

[# if items.length]
  <ul>
    [# each items as item]
      <li>[= item.name][= item.price | currency]</li>
    [/each]
  </ul>
[# else]
  <p>No items yet.</p>
[/if]

Render from a Controller

// modules/profile/profile.controller.js
export async function show(req, { userService }) {
  const user = await userService.findById(req.user.id)
  req.render('profile', {
    title: 'My Profile',
    user,
  })
}

Global Config (settings.js)

// views/settings.js
export default {
  layout:  'layout',              // every page wrapped in layout.html
  globals: {
    siteName: 'My Site',
    year:     new Date().getFullYear(),
  },
}
Tip — Globals defined in settings.js are available in every template without passing them from the controller.

Mix & Match

You're not limited to one mode. A single SpaceNode app can serve static files, expose a JSON API, and render SSR pages — all at the same time:

const app = await createApp({
  static:  './public',     // serve CSS / JS / images
  views:   './views',      // enable SSR template engine
  pipe:    ['cors'],        // enable CORS for API routes
  baseUrl: import.meta.url,
})

Then modules can freely mix JSON responses (req.send()) and rendered pages (req.render()) within the same project.

Quick Comparison

Static Site REST API SSR Site
Config key static: './public' pipe: ['cors'] views: './views'
Modules needed None Yes Yes
Response type Files (HTML, CSS, JS) JSON Rendered HTML
SEO Full (pre-built HTML) N/A Full (server-rendered)
Build step Optional (SPA bundlers) None None
Auth & validation defineAuth(), DTOs Guards, cookies, CSRF
Detailed guide Static Site → REST API → SSR Site →