Skip to content

Sessions

Shokupan provides session management compatible with connect/express-session stores.

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();
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'
}
}));

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.

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"
}))

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",
}));

The session methods (regenerate, destroy, save, reload) return a Promise, allowing you to use await or .then().

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 };
});
app.get('/cart', (ctx) => {
return {
cart: ctx.session.cart || []
};
});
app.post('/logout', async (ctx) => {
// Destroy session
await ctx.session.destroy();
return { message: 'Logged out' };
});
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);
});
// Login
app.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 route
const 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;
});
// Logout
app.post('/logout', (ctx) => {
ctx.session.destroy();
return { message: 'Logged out' };
});
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 };
});
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 };
});
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
}
}));

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 safety
app.get('/profile', (ctx) => {
const userId = ctx.session.userId; // Typed as string | undefined
});