Skip to content

Commit 0a99120

Browse files
committed
docs(en): realign guides to v0.15 context namespace API
- Consolidate response helpers into download and empty - Document ctx.get, ctx.set, and ctx.send namespaces - Document timeout 503 and template limit 400 statuses - Drop ctx.state in favor of signed session and validated - Move worker pool guide to recipes - Remove wildcard wording from route patterns description - Rename lifecycle events and add security event group - Rename routesDir to routes.directory across config - Rename WrapMware to Wrap.apply and Validator.read to ctx.get.validated
1 parent b672b94 commit 0a99120

69 files changed

Lines changed: 2171 additions & 2051 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/by-design/bearer-auth.md

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,23 @@ Bearer is the opposite. The token format, the signature, and the trust source al
2222

2323
A token guard is a small composition over parts that already ship:
2424

25-
- **Read the header** - [`ctx.header('authorization')`](/core-concepts/context-object#request-data-access) returns the raw `Authorization` value.
25+
- **Read the header** - [`ctx.get.header('authorization')`](/core-concepts/context-object#ctx-get-header-key) returns the raw `Authorization` value.
2626
- **Run early** - [global middleware](/middleware/global) runs before route handlers and can stop a request by returning a `Response`.
2727
- **Reject cleanly** - [`ctx.handleError(401, ...)`](/core-concepts/context-object#error-handling) routes through [`router.catch()`](/error-handling/object-details) when one is set.
28-
- **Carry the result** - [`ctx.state`](/core-concepts/context-object#sharing-state) hands the decoded identity to the handler downstream.
28+
- **Carry the result** - [`ctx.set.session(...)`](/core-concepts/context-object#ctx-set-session-data) signs the decoded identity into a cookie the handler reads back, covered in [session middleware](/middleware/session).
2929

3030
## A Bearer Guard
3131

32-
This middleware pulls the token out of the header, verifies it, and stores the result for later handlers. The `verifyToken` placeholder stands in for the scheme of choice, a JWT check, a JWKS lookup, or an introspection call.
32+
This middleware pulls the token out of the header, verifies it, and stores the result for later handlers. The `verifyToken` placeholder stands in for the scheme of choice, a JWT check, a JWKS lookup, or an introspection call. Storing the identity needs the [session middleware](/middleware/session) registered first.
3333

3434
```typescript twoslash
35-
import type { Context } from '@neabyte/deserve'
36-
import { Router } from '@neabyte/deserve'
35+
import { Router, type Context } from '@neabyte/deserve'
3736

3837
const router = new Router()
3938
declare function verifyToken(token: string): Promise<{ userId: string } | null>
4039
// ---cut---
4140
router.use(async (ctx, next) => {
42-
const header = ctx.header('authorization')
41+
const header = ctx.get.header('authorization')
4342
const spaceIndex = header ? header.indexOf(' ') : -1
4443
const scheme = spaceIndex > 0 ? header!.slice(0, spaceIndex) : ''
4544

@@ -56,47 +55,46 @@ router.use(async (ctx, next) => {
5655
}
5756

5857
// Hand the identity to handlers
59-
ctx.state.userId = claims.userId
58+
await ctx.set.session({ userId: claims.userId })
6059
return await next()
6160
})
6261

6362
await router.serve(8000)
6463
```
6564

66-
The handler then reads the identity straight from state, with no token parsing of its own.
65+
The handler then reads the identity straight from the session, with no token parsing of its own.
6766

6867
```typescript twoslash
6968
import type { Context } from '@neabyte/deserve'
7069
// ---cut---
7170
export function GET(ctx: Context): Response {
7271
// Read what the guard stored
73-
const userId = ctx.state.userId
74-
return ctx.send.json({ userId })
72+
const session = ctx.get.session()
73+
return ctx.send.json({ userId: session?.userId })
7574
}
7675
```
7776

7877
## Routing Failures Through One Handler
7978

80-
The guard above returns the `401` from inside the middleware. To send every auth failure through one place, wrap the middleware with [`WrapMware`](/middleware/global#wrapping-middleware-with-error-handling) and throw on rejection, then shape the reply with [`router.catch()`](/error-handling/object-details).
79+
The guard above returns the `401` from inside the middleware. To send every auth failure through one place, wrap the middleware with [`Wrap.apply`](/middleware/global#wrapping-middleware-with-error-handling) and throw on rejection, then shape the reply with [`router.catch()`](/error-handling/object-details).
8180

8281
```typescript twoslash
83-
import type { Context } from '@neabyte/deserve'
84-
import { Router, WrapMware } from '@neabyte/deserve'
82+
import { Router, Wrap, type Context } from '@neabyte/deserve'
8583

8684
const router = new Router()
8785
declare function verifyToken(token: string): Promise<{ userId: string } | null>
8886
// ---cut---
8987
// Throws reach router.catch when wrapped
90-
const bearer = WrapMware('Bearer', async (ctx: Context, next) => {
91-
const header = ctx.header('authorization')
88+
const bearer = Wrap.apply('Bearer', async (ctx: Context, next) => {
89+
const header = ctx.get.header('authorization')
9290
if (!header?.toLowerCase().startsWith('bearer ')) {
9391
throw new Error('Missing Bearer token')
9492
}
9593
const claims = await verifyToken(header.slice(7).trim())
9694
if (!claims) {
9795
throw new Error('Invalid token')
9896
}
99-
ctx.state.userId = claims.userId
97+
await ctx.set.session({ userId: claims.userId })
10098
return await next()
10199
})
102100

@@ -121,22 +119,21 @@ This is the same wrapping pattern [Basic Auth](/middleware/basic-auth) uses inte
121119
A token guard often belongs on an API prefix while public pages stay open. Path-specific middleware scopes the check to one prefix, the same form shown in [global middleware](/middleware/global#path-specific-middleware).
122120

123121
```typescript twoslash
124-
import type { Context } from '@neabyte/deserve'
125-
import { Router } from '@neabyte/deserve'
122+
import { Router, type Context } from '@neabyte/deserve'
126123

127124
const router = new Router()
128125
declare function verifyToken(token: string): Promise<{ userId: string } | null>
129126
// ---cut---
130127
// Guard only the /api routes
131128
router.use('/api', async (ctx, next) => {
132-
const header = ctx.header('authorization')
129+
const header = ctx.get.header('authorization')
133130
const claims = header?.toLowerCase().startsWith('bearer ')
134131
? await verifyToken(header.slice(7).trim())
135132
: null
136133
if (!claims) {
137134
return await ctx.handleError(401, new Error('Invalid token'))
138135
}
139-
ctx.state.userId = claims.userId
136+
await ctx.set.session({ userId: claims.userId })
140137
return await next()
141138
})
142139
```

docs/by-design/cache.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import type { Context } from '@neabyte/deserve'
2929
const cache = new Map<string, unknown>()
3030

3131
export async function GET(ctx: Context): Promise<Response> {
32-
const key = ctx.pathname
32+
const key = ctx.get.pathname()
3333

3434
// Serve the cached value when present
3535
const hit = cache.get(key)
@@ -40,7 +40,7 @@ export async function GET(ctx: Context): Promise<Response> {
4040
})
4141
}
4242

43-
// Build it once, then store for next time
43+
// Build once, then store for reuse
4444
const data = await buildExpensiveData()
4545
cache.set(key, data)
4646
return ctx.send.json({
@@ -64,10 +64,10 @@ const ttlMs = 30_000
6464
const cache = new Map<string, { value: unknown, expiresAt: number }>()
6565
6666
export function GET(ctx: Context): Response {
67-
const key = ctx.pathname
67+
const key = ctx.get.pathname()
6868
const entry = cache.get(key)
6969
70-
// Fresh entry wins, expired one is dropped
70+
// Fresh entry wins, expired one drops
7171
if (entry && Date.now() < entry.expiresAt) {
7272
return ctx.send.json({
7373
source: 'cache',
@@ -96,4 +96,4 @@ Two cases call for more than a process-local map. A cache that must survive a re
9696

9797
## Per-Request Sharing
9898

99-
Caching across requests is one need, passing a value along a single request is another. A value computed in middleware and read by the handler does not belong in a cache at all, it belongs in [`ctx.state`](/core-concepts/context-object#sharing-state), which lives for exactly one request and is gone when the response is sent.
99+
Caching across requests is one need, passing a value along a single request is another. A value computed in middleware and read by the handler does not belong in a cache at all. For per-user identity the signed [session](/middleware/session) carries it through `ctx.set.session()` and `ctx.get.session()`, and for validated input the [validate middleware](/middleware/validation/overview) hands it on through `ctx.get.validated()`. Anything else the handler re-derives from the request it already holds.

docs/by-design/compress.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,15 @@ To keep one response uncompressed, set a header the runtime treats as a stop sig
5656
import type { Context } from '@neabyte/deserve'
5757
// ---cut---
5858
export function GET(ctx: Context): Response {
59-
// Block any layer from rewriting the body
60-
ctx.setHeader('Cache-Control', 'no-transform')
59+
// Block any layer from rewriting body
60+
ctx.set.header('Cache-Control', 'no-transform')
6161
return ctx.send.json({
6262
message: 'sent verbatim'
6363
})
6464
}
6565
```
6666

67-
Setting headers through [`ctx.setHeader`](/core-concepts/context-object#response-headers) is the same path used everywhere else, so this opt-out reads like any other header.
67+
Setting headers through [`ctx.set.header`](/core-concepts/context-object#ctx-set-header-key-value) is the same path used everywhere else, so this opt-out reads like any other header.
6868

6969
## Already-Encoded Bodies
7070

docs/by-design/https-redirect.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,21 @@ await router.serve(8000)
3838

3939
## Reading the Real Scheme
4040

41-
When the app does need to know whether the client used HTTPS, the answer rides in a forwarded header the proxy sets, not in the local connection. A trusted proxy adds [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto), read through [`ctx.header`](/core-concepts/context-object#request-data-access).
41+
When the app does need to know whether the client used HTTPS, the answer rides in a forwarded header the proxy sets, not in the local connection. A trusted proxy adds [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto), read through [`ctx.get.header`](/core-concepts/context-object#ctx-get-header-key).
4242

4343
```typescript twoslash
4444
import type { Context } from '@neabyte/deserve'
4545
// ---cut---
4646
export function GET(ctx: Context): Response {
4747
// Scheme the client actually used
48-
const proto = ctx.header('x-forwarded-proto') ?? 'http'
48+
const proto = ctx.get.header('x-forwarded-proto') ?? 'http'
4949
return ctx.send.json({
5050
secure: proto === 'https'
5151
})
5252
}
5353
```
5454

55-
Trust this header only behind a proxy configured through [`trustProxy`](/getting-started/server-configuration#client-ip-resolution), the same trust boundary [`ctx.ip`](/by-design/request-id#the-ip-is-the-source-of-truth) relies on. An untrusted client can set any header, so the value means nothing without that boundary.
55+
Trust this header only behind a proxy configured through [`trustProxy`](/getting-started/server-configuration#client-ip-resolution), the same trust boundary [`ctx.get.ip()`](/core-concepts/context-object#ctx-get-ip-options) relies on. An untrusted client can set any header, so the value means nothing without that boundary.
5656

5757
## Serving HTTPS Directly
5858

docs/by-design/locale-redirect.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ Language choice is a product decision, not a transport rule. Which locales exist
1414

1515
## Reading the Preference
1616

17-
The browser sends its language list in the [`Accept-Language`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language) header, read through [`ctx.header`](/core-concepts/context-object#request-data-access). A small match against the locales the app supports gives the target, then [`ctx.send.redirect`](/response/redirect) sends the visitor there.
17+
The browser sends its language list in the [`Accept-Language`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language) header, read through [`ctx.get.header`](/core-concepts/context-object#ctx-get-header-key). A small match against the locales the app supports gives the target, then [`ctx.send.redirect`](/response/redirect) sends the visitor there.
1818

1919
```typescript twoslash
2020
import type { Context } from '@neabyte/deserve'
2121
// ---cut---
2222
export function GET(ctx: Context): Response {
2323
// Read the browser language hint
24-
const header = ctx.header('accept-language') ?? ''
24+
const header = ctx.get.header('accept-language') ?? ''
2525
const supported = ['en', 'id']
2626

2727
// Match a supported locale or default
@@ -37,7 +37,7 @@ A 302 keeps the redirect temporary, so a later visit can still be matched again.
3737

3838
## Sharing the Choice With Later Routes
3939

40-
When several routes need the resolved locale, middleware can resolve it once and store it in [`ctx.state`](/core-concepts/context-object#sharing-state) instead of redirecting, so each handler reads the same value.
40+
When several routes need the resolved locale, middleware can resolve it once and store it in the signed [session](/middleware/session) instead of redirecting, so each handler reads the same value through `ctx.get.session()`.
4141

4242
```typescript twoslash
4343
import { Router } from '@neabyte/deserve'
@@ -46,15 +46,16 @@ const router = new Router()
4646
// ---cut---
4747
router.use(async (ctx, next) => {
4848
// Resolve locale once for the request
49-
const header = ctx.header('accept-language') ?? ''
49+
const header = ctx.get.header('accept-language') ?? ''
5050
const preferred = header.split(',')[0]?.slice(0, 2) ?? 'en'
5151

5252
// Share it with the route handlers
53-
ctx.state.locale = ['en', 'id'].includes(preferred) ? preferred : 'en'
53+
const locale = ['en', 'id'].includes(preferred) ? preferred : 'en'
54+
await ctx.set.session({ locale })
5455
return await next()
5556
})
5657

5758
await router.serve(8000)
5859
```
5960

60-
The redirect form sends the visitor to a localized URL, while the state form keeps one URL and passes the locale inward. Both stay in plain route files, so the rule is where the language matters and nowhere else.
61+
The redirect form sends the visitor to a localized URL, while the session form keeps one URL and passes the locale inward. Both stay in plain route files, so the rule is where the language matters and nowhere else.

docs/by-design/method-override.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ Deserve also routes on the real `req.method`, which a handler cannot rewrite mid
2222

2323
## Every Method Is a Route
2424

25-
A route file exports one function per method, and the name is the method. There is no table to register and no verb to translate. A file like `items/[id].ts` reads its `id` from the path through [`ctx.param`](/core-concepts/context-object#request-data-access).
25+
A route file exports one function per method, and the name is the method. There is no table to register and no verb to translate. A file like `items/[id].ts` reads its `id` from the path through [`ctx.get.param`](/core-concepts/context-object#ctx-get-param-key).
2626

2727
```typescript twoslash
2828
import type { Context } from '@neabyte/deserve'
2929
// ---cut---
3030
// Read one item by id
3131
export function GET(ctx: Context): Response {
3232
return ctx.send.json({
33-
id: ctx.param('id')
33+
id: ctx.get.param('id')
3434
})
3535
}
3636

@@ -67,7 +67,7 @@ await fetch(
6767
)
6868
```
6969

70-
Building stateless or stateful is the same move, just drop the files. A stateless REST endpoint is a handler that reads the request and replies, while a stateful flow adds the [session middleware](/middleware/session) and reads per-user data from [`ctx.state`](/core-concepts/context-object#sharing-state). The method stays real either way, with nothing to disguise on the way in.
70+
Building stateless or stateful is the same move, just drop the files. A stateless REST endpoint is a handler that reads the request and replies, while a stateful flow adds the [session middleware](/middleware/session) and reads per-user data through `ctx.get.session()`. The method stays real either way, with nothing to disguise on the way in.
7171

7272
A full REST or RESTful API falls out of this with no extra wiring. The verbs already line up with the actions, `GET` to read, `POST` to create, `PUT` and `PATCH` to update, `DELETE` to remove, so a resource is just a route file with those handlers. The behavior reads the same across every endpoint, which is what makes the whole API feel seamless.
7373

0 commit comments

Comments
 (0)