Sessions
Shokupan provides session management compatible with connect/express-session stores.
Basic Usage
Section titled “Basic Usage”import { Shokupan, Session } from 'shokupan';
const app = new Shokupan();
app.use(Session({ secret: 'your-secret-key'}));
app.get('/login', (ctx) => { ctx.session.user = { id: '123', name: 'Alice' }; return { message: 'Logged in' };});
app.get('/profile', (ctx) => { if (!ctx.session.user) { return ctx.json({ error: 'Not authenticated' }, 401); } return ctx.session.user;});
app.get('/logout', (ctx) => { ctx.session.destroy(); return { message: 'Logged out' };});
app.listen();Configuration
Section titled “Configuration”app.use(Session({ secret: 'your-secret-key', // Required name: 'sessionId', // Cookie name (default: 'connect.sid') resave: true, // Resave session even if unmodified (default: true) saveUninitialized: true, // Save new sessions (default: true)
cookie: { httpOnly: true, secure: true, // HTTPS only maxAge: 24 * 60 * 60 * 1000, // 24 hours sameSite: 'lax' }}));External Stores (Redis, Database)
Section titled “External Stores (Redis, Database)”For production, you should use an external store like SurrealDB or Redis. The Session plugin is compatible with stores that follow the connect store interface. You can find most compatible stores here.
SurrealDB Example
Section titled “SurrealDB Example”Using connect-surreal:
import { SurrealDBStore } from "connect-surreal"import { Shokupan, Session } from "shokupan";
const app = new Shokupan();
app.use(Session({ store: new SurrealDBStore({ url: 'ws://localhost:8000', signinOpts: { username: 'root', password: 'root', }, connectionOpts: { namespace: 'main', database: 'main', }, // SurrealDB doesn't support record TTL, this option regularly deletes expired sessions. autoSweepExpired: true }), secret: "keyboard cat"}))Redis Example
Section titled “Redis Example”Using connect-redis and ioredis:
import { RedisStore } from "connect-redis"import { Redis } from "ioredis"import { Shokupan, Session } from "shokupan";
const app = new Shokupan();
app.use(Session({ store: new RedisStore({ prefix: "myapp:", client: new Redis(), }), secret: "keyboard cat",}));Session Methods
Section titled “Session Methods”The session methods (regenerate, destroy, save, reload) return a Promise, allowing you to use await or .then().
Set Data
Section titled “Set Data”app.post('/cart/add', async (ctx) => { const { productId } = await ctx.body();
if (!ctx.session.cart) { ctx.session.cart = []; }
ctx.session.cart.push(productId);
return { cart: ctx.session.cart };});Get Data
Section titled “Get Data”app.get('/cart', (ctx) => { return { cart: ctx.session.cart || [] };});Destroy Session
Section titled “Destroy Session”app.post('/logout', async (ctx) => { // Destroy session await ctx.session.destroy(); return { message: 'Logged out' };});Regenerate Session
Section titled “Regenerate Session”app.post('/login', async (ctx) => { const { username, password } = await ctx.body();
// Validate credentials const user = await validateUser(username, password);
if (user) { // Regenerate session ID (security best practice) await ctx.session.regenerate();
ctx.session.user = user; return { message: 'Logged in' }; }
return ctx.json({ error: 'Invalid credentials' }, 401);});Common Patterns
Section titled “Common Patterns”Authentication
Section titled “Authentication”// Loginapp.post('/login', async (ctx) => { const { email, password } = await ctx.body();
const user = await authenticateUser(email, password);
if (!user) { return ctx.json({ error: 'Invalid credentials' }, 401); }
ctx.session.userId = user.id; ctx.session.email = user.email;
return { user };});
// Protected routeconst requireAuth = async (ctx, next) => { if (!ctx.session.userId) { return ctx.json({ error: 'Unauthorized' }, 401); }
ctx.state.user = await getUserById(ctx.session.userId); return next();};
app.get('/profile', requireAuth, (ctx) => { return ctx.state.user;});
// Logoutapp.post('/logout', (ctx) => { ctx.session.destroy(); return { message: 'Logged out' };});Shopping Cart
Section titled “Shopping Cart”app.get('/cart', (ctx) => { return { items: ctx.session.cart || [] };});
app.post('/cart', async (ctx) => { const { productId, quantity } = await ctx.body();
if (!ctx.session.cart) { ctx.session.cart = []; }
ctx.session.cart.push({ productId, quantity });
return { cart: ctx.session.cart };});Flash Messages
Section titled “Flash Messages”app.post('/submit', async (ctx) => { // Process form
ctx.session.flash = { type: 'success', message: 'Form submitted successfully' };
return ctx.redirect('/dashboard');});
app.get('/dashboard', (ctx) => { const flash = ctx.session.flash; delete ctx.session.flash; // Remove after reading
return { flash };});Security Best Practices
Section titled “Security Best Practices”app.use(Session({ secret: process.env.SESSION_SECRET!, // Strong, random secret
resave: false, saveUninitialized: false,
cookie: { httpOnly: true, // Prevent XSS secure: process.env.NODE_ENV === 'production', // HTTPS only sameSite: 'strict', // CSRF protection maxAge: 60 * 60 * 1000 // 1 hour }}));TypeScript Types
Section titled “TypeScript Types”Type your session data:
import { ShokupanContext } from 'shokupan';
interface SessionData { userId?: string; email?: string; cart?: Array<{ productId: string; quantity: number }>;}
declare module 'shokupan' { interface ShokupanContext { session: SessionData & { destroy: () => void; regenerate: () => Promise<void>; }; }}
// Now you have type safetyapp.get('/profile', (ctx) => { const userId = ctx.session.userId; // Typed as string | undefined});Next Steps
Section titled “Next Steps”- Authentication - OAuth2 support
- Rate Limiting - Protect login endpoints
- Security Headers - Add security headers