191 lines
7.2 KiB
HTML
191 lines
7.2 KiB
HTML
<!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/<name></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/<name>.ts</code>. All route files follow the same skeleton:</p>
|
|
<pre><code>import { Router } from 'express';
|
|
import { requireAuth } from 'auth-connector/server/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 authenticated user</h3>
|
|
<p>
|
|
<code>req.auth</code> is typed via a <code>declare module</code> in
|
|
<code>source/auth-connector/source/server/auth.ts</code>. The available fields are
|
|
<code>userId</code>, <code>email</code>, <code>roles</code>, and <code>products</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.auth!.userId;
|
|
const email = req.auth!.email;</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 { field: string }</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 — 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>
|