rojects/workspace/guides/writing-backend-routes/index.html

191 lines
7.2 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Writing Backend Routes — Roject</title>
<link rel="stylesheet" href="../../_assets_/styles.css">
<link rel="stylesheet" href="../../_assets_/nav.css">
</head>
<body>
<div class="page">
<header>
<h1>Writing Backend Routes</h1>
<p class="subtitle">How to add a new API endpoint to the Express server: file layout, auth, request handling, file storage, and mounting.</p>
</header>
<section>
<h2>Summary</h2>
<div class="card">
<p>
Every API feature lives in its own router file under <code>server/routes/</code>.
The router is created with Express's <code>Router()</code>, handlers are attached
to it, and it is exported and mounted in <code>server/index.ts</code> at an
<code>/api/&lt;name&gt;</code> path. The server runs via <code>ts-node</code>
— there is no separate compilation step for server-side TypeScript.
See <code>server/routes/layout.ts</code> (simple JSON CRUD) and
<code>server/routes/rojos.ts</code> (streaming) as reference implementations.
</p>
</div>
</section>
<section>
<h2>Steps</h2>
<div class="card">
<h3>1 — Create the route file</h3>
<p>Create <code>server/routes/&lt;name&gt;.ts</code>. All route files follow the same skeleton:</p>
<pre><code>import { Router } from 'express';
import { requireAuth } from '../middleware/auth';
const router = Router();
router.use( requireAuth );
router.get( '/', ( req, res ) =>
{
res.json( { ok: true } );
} );
export default router;</code></pre>
<p style="margin-top:0.75rem">
<code>requireAuth</code> is applied with <code>router.use</code> so it covers
every handler in the file. If only some routes need auth, apply it per-handler
as a middleware argument instead.
</p>
</div>
<div class="card">
<h3>2 — Access the session</h3>
<p>
The session is typed via a <code>declare module</code> in
<code>server/middleware/auth.ts</code>. The available fields are
<code>req.session.userId</code> and <code>req.session.username</code>, both
<code>string | undefined</code>. After <code>requireAuth</code> they are
guaranteed to be set — use the non-null assertion (<code>!</code>) freely:
</p>
<pre><code>const userId = req.session.userId!;
const username = req.session.username!;</code></pre>
</div>
<div class="card">
<h3>3 — Read request data</h3>
<p>
Both body parsers are applied globally in <code>server/index.ts</code>
— no per-route setup is needed:
</p>
<ul style="margin-top:0.5rem">
<li><strong>JSON body</strong> (<code>Content-Type: application/json</code>) — available as <code>req.body</code>, cast to your type: <code>req.body as &#123; field: string &#125;</code></li>
<li><strong>Plain text body</strong> (<code>Content-Type: text/plain</code>) — available as <code>req.body</code> (a string). Used for raw file content saves.</li>
<li><strong>Query params</strong><code>req.query.paramName as string</code></li>
<li><strong>Route params</strong><code>req.params.paramName</code></li>
</ul>
</div>
<div class="card">
<h3>4 — Store data as JSON files</h3>
<p>
There is no database. Persistent data goes in subdirectories of
<code>storage/</code>. Reference the path relative to the compiled output
location using <code>__dirname</code>:
</p>
<pre><code>import fs from 'fs';
import path from 'path';
const DATA_DIR = path.join( __dirname, '..', '..', 'storage', 'my-feature' );
function ensureDir(): void
{
if ( !fs.existsSync( DATA_DIR ) ) fs.mkdirSync( DATA_DIR, { recursive: true } );
}
function readData( id: string ): any
{
ensureDir();
const fp = path.join( DATA_DIR, id + '.json' );
if ( !fs.existsSync( fp ) ) return null;
return JSON.parse( fs.readFileSync( fp, 'utf8' ) );
}
function writeData( id: string, data: any ): void
{
ensureDir();
fs.writeFileSync( path.join( DATA_DIR, id + '.json' ), JSON.stringify( data ), 'utf8' );
}</code></pre>
<p style="margin-top:0.75rem">
IDs should be UUIDs from <code>crypto.randomUUID()</code>, never user-supplied
strings used directly as filenames. If you must derive a filename from user
input, sanitise it: <code>s.replace( /[^a-zA-Z0-9_-]/g, '_' )</code>.
</p>
</div>
<div class="card">
<h3>5 — Return responses</h3>
<p>Standard patterns used across the codebase:</p>
<pre><code>res.json( { ok: true } ); // success, no payload
res.json( data ); // success with payload
res.status( 400 ).json( { error: 'Bad input' } ); // client error
res.status( 401 ).json( { error: 'Not authenticated' } ); // handled by requireAuth
res.status( 404 ).json( { error: 'Not found' } );
res.status( 500 ).json( { error: String( err ) } );</code></pre>
<p style="margin-top:0.75rem">
Always <code>return</code> after sending a response inside a conditional block
to prevent Express from throwing "headers already sent":
</p>
<pre><code>if ( !id ) { res.status( 400 ).json( { error: 'Missing id' } ); return; }</code></pre>
</div>
<div class="card">
<h3>6 — Streaming responses</h3>
<p>
For SSE / streaming (e.g. AI output), set the headers explicitly, flush them,
write newline-delimited JSON chunks, then end the response:
</p>
<pre><code>res.setHeader( 'Content-Type', 'text/event-stream' );
res.setHeader( 'Cache-Control', 'no-cache' );
res.setHeader( 'Connection', 'keep-alive' );
res.flushHeaders();
for await ( const chunk of someStream )
{
res.write( JSON.stringify( { type: 'DATA', value: chunk } ) + '\n' );
}
res.write( JSON.stringify( { type: 'DONE' } ) + '\n' );
res.end();</code></pre>
<p style="margin-top:0.75rem">
Wrap the whole block in <code>try/catch</code> and check
<code>!res.headersSent</code> before sending an error response, since headers
may already be flushed by the time an exception occurs.
</p>
</div>
<div class="card">
<h3>7 — Mount in server/index.ts</h3>
<p>
Open <code>server/index.ts</code> and add two lines — an import and a
<code>app.use</code> call — following the existing pattern:
</p>
<pre><code>import myRouter from './routes/my-feature';
app.use( '/api/my-feature', myRouter );</code></pre>
<p style="margin-top:0.75rem">
Restart the server after this change. No build step is needed —
<code>ts-node</code> compiles on the fly.
</p>
</div>
</section>
<footer>
Roject &mdash; writing backend routes
</footer>
</div>
<script>var NAV_ROOT = '../../';</script>
<script src="../../_assets_/nav-data.js"></script>
<script src="../../_assets_/nav.js"></script>
</body>
</html>