@dotpress/core 0.0.0-stage → 2.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Renaud Guyon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,134 @@
1
- # Temporary Holding Version
1
+ # @dotpress/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Lightweight, decorator-free TypeScript framework for end-to-end typed HTTP APIs.
4
+
5
+ The core is independent of the HTTP server: it compiles your routes into a request pipeline that an adapter (Express, Hono, Fastify…) mounts on its server. It only relies on Web-standard APIs.
6
+
7
+ > 🚧 Dotpress V2 is in alpha (`2.0.0-alpha`): the API may still change before 2.0.0.
8
+
9
+ ```ts
10
+ import { createApp, defineRoute, notFoundError, sse } from '@dotpress/core'
11
+
12
+ defineRoute({
13
+ method: 'post',
14
+ path: '/orgs/:orgId/members',
15
+ status: 201,
16
+ schema: (z) => ({
17
+ body: z.object({ name: z.string(), age: z.number().min(13) }),
18
+ response: z.object({ id: z.string() }),
19
+ }),
20
+ handler: async ({ req }) => {
21
+ // req.params: { orgId: string }, req.body: { name: string; age: number }
22
+ if (req.body.name === 'unknown') return notFoundError()
23
+ return { id: '...' }
24
+ },
25
+ })
26
+
27
+ defineRoute({
28
+ path: '/orders/live',
29
+ // Server-Sent Events, stopped when the client disconnects
30
+ handler: () =>
31
+ sse(async function* ({ signal }) {
32
+ for await (const order of watchOrders({ signal })) {
33
+ yield { event: 'order', id: order.id, data: order }
34
+ }
35
+ }),
36
+ })
37
+
38
+ const app = await createApp({ adapter: /* an HTTP adapter */ })
39
+ await app.listen(3000)
40
+ ```
41
+
42
+ ## Repeated fields
43
+
44
+ A query parameter, form field or file sent once gives a single value; sent several times, an array. `arrayOf()` always gives an array:
45
+
46
+ ```ts
47
+ import { arrayOf } from '@dotpress/core'
48
+
49
+ schema: (z) => ({
50
+ query: z.object({ tag: arrayOf(z.string()).default([]) }),
51
+ files: z.object({ photos: arrayOf(z.array(z.file()).max(5)) }),
52
+ })
53
+ ```
54
+
55
+ ## Route documentation
56
+
57
+ Routes describe themselves for documentation generators (`@dotpress/openapi`): schemas, success `status`, and `meta`:
58
+
59
+ ```ts
60
+ defineRoute({
61
+ path: '/members/:id',
62
+ middlewares: [requireAuth],
63
+ schema: (z) => ({ response: Member }),
64
+ meta: {
65
+ summary: 'Get a member',
66
+ tags: ['members'],
67
+ responses: { 404: 'Member not found' }, // other responses, by status
68
+ },
69
+ handler: ...,
70
+ })
71
+ ```
72
+
73
+ - **Security** comes from the middlewares: a middleware declared with `defineMiddleware(fn, { security: ['bearerAuth'] })` marks the routes using it, directly or through a group, as secured. `meta.security` overrides it (`[]` for a public route). `app.routes[i].security` holds the result.
74
+ - **`meta.responses`** documents the other responses of a route: errors returned by the handler, or bodies that are not JSON (`{ description: 'CSV export', contentType: 'text/csv' }`). An entry is a description, or an object with a `description`, an optional `schema` and an optional `contentType`.
75
+ - **`meta.hidden`** excludes a route from the documentation.
76
+
77
+ ## CORS
78
+
79
+ Cross-origin requests from browsers (CORS) are disabled by default. Enable them with the list of allowed origins:
80
+
81
+ ```ts
82
+ await createApp({
83
+ adapter,
84
+ cors: {
85
+ origin: ['https://app.example.com', /^https:\/\/[a-z]+\.example\.com$/],
86
+ credentials: true, // cookies, fetch(..., { credentials: 'include' })
87
+ exposedHeaders: ['x-request-id'],
88
+ maxAge: 600, // preflight cache, in seconds
89
+ },
90
+ })
91
+ ```
92
+
93
+ | Option | Description |
94
+ | --- | --- |
95
+ | `origin` | Required. `'*'` (any origin, without credentials), an origin (`'https://app.example.com'`, lowercase, without trailing slash), a regular expression, a list of origins and regular expressions, or a function `(origin, request) => boolean \| Promise<boolean>`. A failing function is logged and refuses the origin. |
96
+ | `credentials` | Allows cookies and credentialed requests. Cannot be combined with `'*'`. Defaults to `false`. |
97
+ | `allowedHeaders` | Request headers allowed by preflight responses. Defaults to the headers requested by the browser. |
98
+ | `exposedHeaders` | Response headers readable by the browser, besides the CORS-safelisted ones. Defaults to none. |
99
+ | `maxAge` | How long browsers may cache preflight responses, in seconds. |
100
+
101
+ - **Preflight requests** (`OPTIONS` with `Origin` and `Access-Control-Request-Method`) from an allowed origin get a `204` response listing the methods of the routes of the path, before the middlewares (authentication) and explicit `OPTIONS` routes. From other origins or for unknown paths, they are handled as usual (`405`, `404`).
102
+ - **Every response** of Dotpress to an allowed origin gets the CORS headers, errors included (`401`, `404`, `422`, `500`…), so that the browser can read them. Responses to other origins are sent without CORS headers: the browser blocks them. The server does not reject them, protection against cross-site requests is the job of authentication (see the cookie mode of `@dotpress/auth`).
103
+ - `Vary: Origin` is added to every response, unless `origin` is `'*'`, so that HTTP caches do not mix up origins.
104
+ - The CORS headers are set before the `onResponse` hooks, which see the final response.
105
+ - Plugins read the settings with `api.cors` (`credentials`, and `isOriginAllowed(req)`): `@dotpress/auth` trusts the allowed origins for its protection against cross-site request forgery when `credentials` is `true`.
106
+ - Native routes of the HTTP server (Express, Fastify) are not covered. Behind API Gateway or a Lambda Function URL, configure CORS either in AWS or in Dotpress, not both: AWS answers preflight requests before the function.
107
+
108
+ ## Base path
109
+
110
+ To serve the app under a sub-path, for instance `https://example.com/my-app/api/users` behind a reverse proxy shared by several apps, use one of these options, depending on what the proxy forwards:
111
+
112
+ ```ts
113
+ // The proxy forwards the full path (nginx: proxy_pass http://app;)
114
+ await createApp({ adapter, basePath: '/my-app/api' })
115
+
116
+ // The proxy removes the prefix (nginx: proxy_pass http://app/;, Traefik: stripPrefix)
117
+ await createApp({ adapter, publicBasePath: '/my-app/api' })
118
+ ```
119
+
120
+ - **`basePath`** prefixes every route, including plugin routes: `GET /users` is served at `/my-app/api/users`, and requests outside of the base path get a `404`. `ctx.req.path` and `app.routes` hold the full paths.
121
+ - **`publicBasePath`** does not change the routing. It only changes the paths seen by the browser: cookie paths set by plugins, documentation URLs, and the paths built with `publicPath()`.
122
+
123
+ The two options cannot be combined. `ctx.publicPath()`, `app.publicPath()` and `api.publicPath()` (in plugins) give the browser path of a route path. Redirects are not prefixed automatically, since they may target a page outside of the API:
124
+
125
+ ```ts
126
+ defineRoute({
127
+ path: '/old',
128
+ handler: (ctx) => redirect(ctx.publicPath('/new')), // Location: /my-app/api/new
129
+ })
130
+ ```
131
+
132
+ ## License
133
+
134
+ MIT