Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
58f5e1c
feat(config): Cookies input source and parser.
RobinTail May 18, 2026
ac63403
feat(server): add cookie-parser dynamic intergration.
RobinTail May 18, 2026
e1adad4
feat(mw): Add cookieMiddleware (public).
RobinTail May 18, 2026
ac3d1da
feat(Documentation): add cookie request parameter depiction based on …
RobinTail May 18, 2026
9493c14
feat(docs): Readme article draft and Changelog 28.1.0.
RobinTail May 18, 2026
a229dcc
feat(deps): Add cookie parser and its types as optional peer dependen…
RobinTail May 19, 2026
e49fe00
fix(server): createCookieParser() to load the cookie-parser peer type…
RobinTail May 19, 2026
111807a
fix(config): Reusing the cookie-parser types to describe options.
RobinTail May 19, 2026
eeeddc3
fix(mw): add jsdoc to cookieMiddleware.
RobinTail May 19, 2026
4ca3be2
feat(mw): createCookieMiddleware.
RobinTail May 19, 2026
519dede
fix(jsdoc): minor.
RobinTail May 19, 2026
34d19bb
ref(server): shortening createCookieParser.
RobinTail May 19, 2026
0eb0fb1
fix(test): placing cookies into makeRequestMock.
RobinTail May 19, 2026
b61fb21
fix(test): shortening for getInput().
RobinTail May 19, 2026
6e3bdb1
fix(test): naming.
RobinTail May 19, 2026
9b910a9
rm(test): redundant in config type.
RobinTail May 19, 2026
fbb196f
fix(test): using testMiddleware for createCookieMiddleware.
RobinTail May 19, 2026
f94a48d
fix(test): simpler test for server parser.
RobinTail May 19, 2026
d164e26
fix(test): rm redundant endpoints from server test.
RobinTail May 19, 2026
2f9fb53
fix(test): simpler mock.
RobinTail May 19, 2026
6b10f17
fix(config): rm second jsdoc.
RobinTail May 19, 2026
d43a7da
fix(test): shortening documentation test.
RobinTail May 19, 2026
75715ee
feat: getSecurityNames() helper for extracting either cookies or head…
RobinTail May 19, 2026
76d7768
minor: naming.
RobinTail May 19, 2026
7075c9b
fix(docs): Readme shortening.
RobinTail May 19, 2026
566032d
fix(docs): upd index.
RobinTail May 19, 2026
326b419
Changelog: 28.1.0.
RobinTail May 19, 2026
3f8168f
Add cookies to dataflow diagram
RobinTail May 19, 2026
cbfc6f9
fix(docs): transparent edges of the diagram.
RobinTail May 19, 2026
3838f1c
rm the plan.
RobinTail May 19, 2026
68561e0
fix(mw): rm double jsdoc and renaming overriding options more clearly.
RobinTail May 19, 2026
9d00043
feat(mw): support object-based values on setCookie.
RobinTail May 19, 2026
baab135
fix(mw): resuing Zod's JSONType.
RobinTail May 19, 2026
29a5367
rm redundant example.
RobinTail May 19, 2026
d7ef9e3
feat: getCookie, alternative flexible approach, versatility reflectin…
RobinTail May 20, 2026
3f5f9a2
Update CHANGELOG.md
RobinTail May 20, 2026
daed650
feat(example): the cookie reading and writing tests.
RobinTail May 20, 2026
14e6768
fix(example): more jsdoc.
RobinTail May 20, 2026
d50b262
fix(example): simpler login.
RobinTail May 20, 2026
466de37
fix(example): better auth assertion.
RobinTail May 20, 2026
9dd8e37
fix(example): required params for login endpoint.
RobinTail May 20, 2026
a1aba6e
fix(mw): clearCookie omits 'expires' and 'maxAge' options, value 1 is…
RobinTail May 20, 2026
4a0a138
fix(docs): Reflecting recommended security measures in Readme.
RobinTail May 20, 2026
aebec21
fix(example): applying best cookie practice on test.
RobinTail May 20, 2026
bbde2f5
fix(example): using scrypt instead of hash sha1.
RobinTail May 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## Version 28

### v28.1.0

- Added support for cookie handling:
- Cookie parsing can be enabled and configured in config (requires to install `cookie-parser`);
- `cookies` and `signedCookies` can be used as `inputSources` in config;
- `createCookieMiddleware()` creates a Middleware that exposes `setCookie()` and `clearCookie()` helpers into context
as well as the `getCookie()` one as an alternative to using cookies within `inputSources`;
- Documentation depicts request parameters when Middleware has `security` schema with `type: cookie`.

### v28.0.1

- Adjusted the list of well-known headers, recognized by Documentation generator:
Expand Down
66 changes: 55 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,18 @@ Start your API server with I/O schema validation and custom middlewares in minut
15. [Child logger](#child-logger)
5. [Advanced features](#advanced-features)
1. [Customizing input sources](#customizing-input-sources)
2. [Headers as input source](#headers-as-an-input-source)
3. [Response customization](#response-customization)
4. [Empty response](#empty-response)
5. [Non-JSON response](#non-json-response) including file downloads
6. [Error handling](#error-handling)
7. [Production mode](#production-mode)
8. [HTML Forms (URL encoded)](#html-forms-url-encoded)
9. [File uploads](#file-uploads)
10. [Connect to your own express app](#connect-to-your-own-express-app)
11. [Testing endpoints](#testing-endpoints)
12. [Testing middlewares](#testing-middlewares)
2. [Headers as an input source](#headers-as-an-input-source)
3. [Cookies](#cookies)
4. [Response customization](#response-customization)
5. [Empty response](#empty-response)
6. [Non-JSON response](#non-json-response) including file downloads
7. [Error handling](#error-handling)
8. [Production mode](#production-mode)
9. [HTML Forms (URL encoded)](#html-forms-url-encoded)
10. [File uploads](#file-uploads)
11. [Connect to your own express app](#connect-to-your-own-express-app)
12. [Testing endpoints](#testing-endpoints)
13. [Testing middlewares](#testing-middlewares)
6. [Integration and Documentation](#integration-and-documentation)
1. [Zod Plugin](#zod-plugin)
2. [End-to-End Type Safety](#end-to-end-type-safety)
Expand Down Expand Up @@ -818,6 +819,49 @@ factory.build({
});
```

## Cookies

Install `cookie-parser` as well as `@types/cookie-parser` and enable `cookies` in your config. To validate cookies add
`"cookies"` and/or `"signedCookies"` to your `inputSources` (the order [matters](#customizing-input-sources)!):

```ts
import { createConfig } from "express-zod-api";

const config = createConfig({
cookies: { secret: "my-secret" }, // or true; the secret enables signedCookies
inputSources: {
get: ["query", "params", "cookies", "signedCookies"], // for methods of your choice
},
});
```
Comment thread
RobinTail marked this conversation as resolved.

Consider `createCookieMiddleware()` that makes a Middleware providing `setCookie()` and `clearCookie()` helpers,
as well as `getCookie()` — alternative to the cookies as an input source:

```ts
import { createCookieMiddleware, Middleware } from "express-zod-api";

const cookieDrivenFactory = factory
.addMiddleware(
createCookieMiddleware({ httpOnly: true, sameSite: "lax", path: "/" }), // recommended base options
)
.addMiddleware(
new Middleware({
security: { type: "cookie", name: "session" }, // improves Documentation
input: z.object({ session: z.string() }), // alternatively, use getCookie
handler: async ({ input: { session }, ctx: { getCookie } }) => {
assert.equal(session, getCookie("session")); // getCookie reads from signedCookies first
},
}),
);

const sessionSettingEndpoint = cookieDrivenFactory.buildVoid({
handler: async ({ ctx: { getCookie, setCookie } }) => {
setCookie("session", "abc123", { httpOnly: false }); // overridden cookie options
},
});
```

## Response customization

`ResultHandler` is responsible for transmitting consistent responses containing the endpoint output or an error.
Expand Down
2 changes: 1 addition & 1 deletion dataflow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions example/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const config = createConfig({
limitError: createHttpError(413, "The file is too large"), // affects uploadAvatarEndpoint
},
compression: true, // affects sendAvatarEndpoint
cookies: true, // for uploadAvatarEndpoint
// third-party middlewares serving their own routes or establishing their own routing besides the API
beforeRouting: ({ app }) => {
app.use(
Expand All @@ -23,6 +24,7 @@ export const config = createConfig({
},
inputSources: {
patch: ["headers", "body", "params"], // affects authMiddleware used by updateUserEndpoint
post: ["body", "params", "files", "cookies"], // cookies for uploadAvatarEndpoint
},
cors: true,
});
Expand All @@ -47,5 +49,6 @@ declare module "express-zod-api" {
files: unknown;
subscriptions: unknown;
forms: unknown;
cookies: unknown;
}
}
31 changes: 31 additions & 0 deletions example/endpoints/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { cookieAssistedFactory } from "../factories.ts";
import { z } from "zod";
import { randomUUID, scrypt } from "node:crypto";
import createHttpError from "http-errors";
import assert from "node:assert/strict";
import { promisify } from "node:util";

/** @desc The endpoint demonstrates setting a cookie */
export const loginEndpoint = cookieAssistedFactory.build({
method: "post",
tag: "cookies",
input: z.object({
username: z.string().trim().nonempty(),
password: z.string().trim().nonempty(),
}),
output: z.object({ message: z.string() }),
handler: async ({ input: { username, password }, ctx: { setCookie } }) => {
const key = await promisify<string, string, number, Buffer>(scrypt)(
password,
"kinda salt",
16,
);
assert(
username === "admin" &&
key.toString("hex") === "79ad19b8c03bc92a2f25ed865400264e",
createHttpError(401, "Invalid credentials"),
);
setCookie("session", { token: randomUUID() });
Comment thread
RobinTail marked this conversation as resolved.
return { message: "Logged in" };
},
});
25 changes: 16 additions & 9 deletions example/endpoints/upload-avatar.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { z } from "zod";
import { defaultEndpointsFactory, ez } from "express-zod-api";
import { ez } from "express-zod-api";
import { createHash } from "node:crypto";
import { cookieAuthenticatedFactory } from "../factories.ts";
import assert from "node:assert/strict";
import createHttpError from "http-errors";

export const uploadAvatarEndpoint = defaultEndpointsFactory.build({
/** @desc The endpoint demonstrates handling a file upload and cookie as an input source */
export const uploadAvatarEndpoint = cookieAuthenticatedFactory.build({
method: "post",
tag: "files",
description: "Handles a file upload.",
Expand All @@ -16,11 +20,14 @@ export const uploadAvatarEndpoint = defaultEndpointsFactory.build({
hash: z.string(),
otherInputs: z.record(z.string(), z.any()),
}),
handler: async ({ input: { avatar, ...rest } }) => ({
name: avatar.name,
size: avatar.size,
mime: avatar.mimetype,
hash: createHash("sha1").update(avatar.data).digest("hex"),
otherInputs: rest,
}),
handler: async ({ input: { avatar, ...rest }, ctx: { session } }) => {
assert(session.token, createHttpError(401, "Unauthorized"));
return {
name: avatar.name,
size: avatar.size,
mime: avatar.mimetype,
hash: createHash("sha1").update(avatar.data).digest("hex"),
otherInputs: rest,
};
},
});
45 changes: 45 additions & 0 deletions example/example.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,38 @@ interface HeadV1UserListNegativeResponseVariants {
400: HeadV1UserListNegativeVariant1;
}

/** post /v1/login */
type PostV1LoginInput = {
username: string;
password: string;
};

/** post /v1/login */
type PostV1LoginPositiveVariant1 = {
status: "success";
data: {
message: string;
};
};

/** post /v1/login */
interface PostV1LoginPositiveResponseVariants {
200: PostV1LoginPositiveVariant1;
}

/** post /v1/login */
type PostV1LoginNegativeVariant1 = {
status: "error";
error: {
message: string;
};
};

/** post /v1/login */
interface PostV1LoginNegativeResponseVariants {
400: PostV1LoginNegativeVariant1;
}

/** get /v1/avatar/send */
type GetV1AvatarSendInput = {
userId: string;
Expand Down Expand Up @@ -291,6 +323,9 @@ interface HeadV1AvatarStreamNegativeResponseVariants {

/** post /v1/avatar/upload */
type PostV1AvatarUploadInput = {
session: {
token: string;
};
avatar: any;
};

Expand Down Expand Up @@ -513,6 +548,7 @@ export type Path =
| "/v1/user/:id"
| "/v1/user/create"
| "/v1/user/list"
| "/v1/login"
| "/v1/avatar/send"
| "/v1/avatar/stream"
| "/v1/avatar/upload"
Expand All @@ -531,6 +567,7 @@ export interface Input {
"post /v1/user/create": PostV1UserCreateInput;
"get /v1/user/list": GetV1UserListInput;
"head /v1/user/list": HeadV1UserListInput;
"post /v1/login": PostV1LoginInput;
/** @deprecated */
"get /v1/avatar/send": GetV1AvatarSendInput;
/** @deprecated */
Expand All @@ -554,6 +591,7 @@ export interface PositiveResponse {
"post /v1/user/create": SomeOf<PostV1UserCreatePositiveResponseVariants>;
"get /v1/user/list": SomeOf<GetV1UserListPositiveResponseVariants>;
"head /v1/user/list": SomeOf<HeadV1UserListPositiveResponseVariants>;
"post /v1/login": SomeOf<PostV1LoginPositiveResponseVariants>;
/** @deprecated */
"get /v1/avatar/send": SomeOf<GetV1AvatarSendPositiveResponseVariants>;
/** @deprecated */
Expand All @@ -577,6 +615,7 @@ export interface NegativeResponse {
"post /v1/user/create": SomeOf<PostV1UserCreateNegativeResponseVariants>;
"get /v1/user/list": SomeOf<GetV1UserListNegativeResponseVariants>;
"head /v1/user/list": SomeOf<HeadV1UserListNegativeResponseVariants>;
"post /v1/login": SomeOf<PostV1LoginNegativeResponseVariants>;
/** @deprecated */
"get /v1/avatar/send": SomeOf<GetV1AvatarSendNegativeResponseVariants>;
/** @deprecated */
Expand Down Expand Up @@ -607,6 +646,8 @@ export interface EncodedResponse {
GetV1UserListNegativeResponseVariants;
"head /v1/user/list": HeadV1UserListPositiveResponseVariants &
HeadV1UserListNegativeResponseVariants;
"post /v1/login": PostV1LoginPositiveResponseVariants &
PostV1LoginNegativeResponseVariants;
/** @deprecated */
"get /v1/avatar/send": GetV1AvatarSendPositiveResponseVariants &
GetV1AvatarSendNegativeResponseVariants;
Expand Down Expand Up @@ -655,6 +696,9 @@ export interface Response {
"head /v1/user/list":
| PositiveResponse["head /v1/user/list"]
| NegativeResponse["head /v1/user/list"];
"post /v1/login":
| PositiveResponse["post /v1/login"]
| NegativeResponse["post /v1/login"];
/** @deprecated */
"get /v1/avatar/send":
| PositiveResponse["get /v1/avatar/send"]
Expand Down Expand Up @@ -702,6 +746,7 @@ export const endpointTags = {
"post /v1/user/create": ["users"],
"get /v1/user/list": ["users"],
"head /v1/user/list": ["users"],
"post /v1/login": ["cookies"],
"get /v1/avatar/send": ["files", "users"],
"head /v1/avatar/send": ["files", "users"],
"get /v1/avatar/stream": ["users", "files"],
Expand Down
Loading