diff --git a/docs/apis.mdx b/docs/apis.mdx
index afc4d0af5..b32cca217 100644
--- a/docs/apis.mdx
+++ b/docs/apis.mdx
@@ -4,37 +4,31 @@ description: 'Building HTTP APIs with Nitric'
# APIs
-Nitric has built-in support for web apps and HTTP API development. The `api` resource allows you to create APIs in your applications, including routing, middleware and request handlers.
+Nitric provides a simple and powerful way to build HTTP APIs in your applications. The `api` resource allows you to create APIs with routing, middleware, request handlers, and security features.
-If you're interested in the architecture, provisioning, or deployment steps, they can be found [here](/architecture/apis).
+## Quick Start
-## Creating APIs
-
-Nitric allows you define named APIs, each with their own routes, middleware, handlers and security.
-
-Here's an example of how to create a new API with Nitric:
+Here's a minimal example to get you started:
```javascript !!
import { api } from '@nitric/sdk'
-// each API needs a unique name
-const galaxyApi = api('far-away-galaxy-api')
+const myApi = api('my-api')
-galaxyApi.get('/moon', async ({ req, res }) => {
- res.body = "that's no moon, it's a space station."
+myApi.get('/hello', async (ctx) => {
+ ctx.res.body = 'Hello World!'
})
```
```typescript !!
import { api } from '@nitric/sdk'
-// each API needs a unique name
-const galaxyApi = api('far-away-galaxy-api')
+const myApi = api('my-api')
-galaxyApi.get('/moon', async ({ req, res }) => {
- res.body = "that's no moon, it's a space station."
+myApi.get('/hello', async (ctx) => {
+ ctx.res.body = 'Hello World!'
})
```
@@ -42,12 +36,11 @@ galaxyApi.get('/moon', async ({ req, res }) => {
from nitric.resources import api
from nitric.application import Nitric
-# each API needs a unique name
-galaxy_api = api('far-away-galaxy-api')
+my_api = api('my-api')
-@galaxy_api.get("/moon")
-async def get_moon(ctx):
- ctx.res.body = "that's no moon, it's a space station."
+@my_api.get("/hello")
+async def hello(ctx):
+ ctx.res.body = "Hello World!"
Nitric.run()
```
@@ -61,9 +54,10 @@ import (
)
func main() {
- galaxyApi := nitric.NewApi("far-away-galaxy-api")
- galaxyApi.Get("/moon", func(ctx *apis.Ctx) {
- ctx.Response.Body = []byte("that's no moon, it's a space station.")
+ myApi := nitric.NewApi("my-api")
+
+ myApi.Get("/hello", func(ctx *apis.Ctx) {
+ ctx.Response.Body = []byte("Hello World!")
})
nitric.Run()
@@ -73,811 +67,388 @@ func main() {
```dart !!
import 'package:nitric_sdk/nitric.dart';
-// each API needs a unique name
-final galaxyApi = Nitric.api("far-away-galaxy-api");
-
-galaxyApi.get("/moon", (ctx) async {
- ctx.res.body = "that's no moon, it's a space station.";
+final myApi = Nitric.api("my-api");
+myApi.get("/hello", (ctx) async {
+ ctx.res.body = "Hello World!";
return ctx;
});
```
-## Routing
+## Core Concepts
-You can define routes and handler services for incoming requests using methods on your API objects.
+### API Resources
-For example, you can declare a route that handles `POST` requests using the `post()` method. When declaring routes you provide the path to match and a callback that will serve as the handler for matching requests.
-
-
- Depending on the language SDK, callbacks are either passed as parameters or
- defined using decorators.
-
+Each API in your application needs a unique name. This name is used to identify the API across your project and in cloud deployments.
```javascript !!
-import { getPlanetList, createPlanet } from 'planets'
-
-galaxyApi.get('/planets', async (ctx) => {
- ctx.res.json(getPlanetList())
-})
-
-galaxyApi.post('/planets', async (ctx) => {
- createPlanet(ctx.req.json())
- ctx.res.status = 201
-})
+const myApi = api('my-api-name')
```
```typescript !!
-import { getPlanetList, createPlanet } from 'planets'
-
-galaxyApi.get('/planets', async (ctx) => {
- ctx.res.json(getPlanetList())
-})
-
-galaxyApi.post('/planets', async (ctx) => {
- createPlanet(ctx.req.json())
- ctx.res.status = 201
-})
+const myApi = api('my-api-name')
```
```python !!
-from planets import get_planets_list, create_planet
-
-@galaxy_api.get("/planets")
-async def list_planets(ctx):
- ctx.res.body = get_planets_list()
-
-@galaxy_api.post("/planets")
-async def create_planet(ctx):
- create_planet(ctx.req.json)
- ctx.res.status = 201
+my_api = api('my-api-name')
```
```go !!
-package main
-
-import (
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/apis"
-)
-
-func main() {
- galaxyApi := nitric.NewApi("far-away-galaxy-api")
-
- galaxyApi.Get("/planets", func(ctx *apis.Ctx) {
- ctx.Response.Headers = map[string][]string{"Content-Type": {"application/json"}}
- ctx.Response.Body = []byte(GetPlanetList())
- })
-
- galaxyApi.Post("/planets", func(ctx *apis.Ctx) {
- CreatePlanet(ctx.Request.Data())
- ctx.Response.Status = 201
- })
-
- nitric.Run()
-}
-
+myApi := nitric.NewApi("my-api-name")
```
```dart !!
-import 'package:planets'
-
-galaxyApi.get("/planets", (ctx) async {
- ctx.res.json(getPlanetList());
-
- return ctx;
-});
-
-galaxyApi.post("/planets", (ctx) async {
- createPlanet(ctx.req.json());
- ctx.res.status = 201;
-
- return ctx;
-});
+final myApi = Nitric.api("my-api-name");
```
### Request Context
-Nitric provides callbacks with a single context object that gives you everything you need to read requests and write responses. By convention this object is typically named `ctx`.
+Every handler receives a context object (`ctx`) that contains:
-The context object includes a request `req` and response `res`, which in turn provide convenient methods for reading and writing bodies, as well as auto-extracted parameters and HTTP headers.
+- `req`: The incoming request object
+- `res`: The response object you'll use to send data back
-#### Parameters
+### Routing
-The path string used to declare routes can include named parameters. The values collected from those parameters are automatically included in the context object under `ctx.req.params`.
-
-Path parameters are denoted by a colon prefix `:`
+APIs support all standard HTTP methods and path-based routing:
```javascript !!
-import { getPlanet } from 'planets'
+const myApi = api('my-api')
-// create a dynamic route and extract the parameter `name`
-galaxyApi.get('/planets/:name', async (ctx) => {
- const { name } = ctx.req.params
- ctx.res.json(getPlanet(name))
+// GET /items
+myApi.get('/items', async (ctx) => {
+ ctx.res.json({ items: [] })
})
-```
-```typescript !!
-import { getPlanet } from 'planets'
-
-// create a dynamic route and extract the parameter `name`
-galaxyApi.get('/planets/:name', async (ctx) => {
- const { name } = ctx.req.params
- ctx.res.json(getPlanet(name))
+// POST /items
+myApi.post('/items', async (ctx) => {
+ const item = ctx.req.json()
+ ctx.res.status = 201
+ ctx.res.json(item)
})
-```
-
-```python !!
-from planets import get_planet
-
-# create a dynamic route and extract the parameter `name`
-@galaxy_api.get("/planets/:name")
-async def get_planet_route(ctx):
- name = ctx.req.params['name']
- ctx.res.body = get_planet(name)
-```
-
-```go !!
-package main
-
-import (
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/apis"
-)
-
-func main() {
- galaxyApi := nitric.NewApi("far-away-galaxy-api")
-
- // create a dynamic route and extract the parameter `name`
- galaxyApi.Get("/planets/:name", func(ctx *apis.Ctx) {
- name := ctx.Request.PathParams()["name"]
-
- ctx.Response.Body = []byte(GetPlanet(name))
- })
- nitric.Run()
-}
-
-```
-
-```dart !!
-import 'package:planets'
-
-// create a dynamic route and extract the parameter `name`
-galaxyApi.get("/planets/:name", (ctx) async {
- final name = ctx.req.pathParams["name"]!;
-
- ctx.res.json(getPlanet(name));
-
- return ctx;
-});
-```
-
-
-
-#### HTTP status and headers
-
-The response object provides `status` and `headers` properties you can use to return HTTP status codes and headers.
-
-
-
-```javascript !!
-// return a redirect response using status 301
-galaxyApi.get('/planets/alderaan', async (ctx) => {
- ctx.res.status = 301
- ctx.res.headers['Location'] = ['https://example.org/debris/alderaan']
+// PUT /items/:id
+myApi.put('/items/:id', async (ctx) => {
+ const { id } = ctx.req.params
+ const item = ctx.req.json()
+ ctx.res.json({ id, ...item })
})
```
```typescript !!
-// return a redirect response using status 301
-galaxyApi.get('/planets/alderaan', async (ctx) => {
- ctx.res.status = 301
- ctx.res.headers['Location'] = ['https://example.org/debris/alderaan']
-})
-```
-
-```python !!
-# return a redirect response using status 301
-@galaxy_api.get("/planets/alderaan")
-async def find_alderaan(ctx):
- ctx.res.status = 301
- ctx.res.headers["Location"] = "https://example.org/debris/alderaan"
-```
+const myApi = api('my-api')
-```go !!
-// return a redirect response using status 301
-galaxyApi.Get("/planets/alderaan", func(ctx *apis.Ctx) {
- ctx.Response.Status = 301
- ctx.Response.Location = "https://example.org/debris/alderaan"
+// GET /items
+myApi.get('/items', async (ctx) => {
+ ctx.res.json({ items: [] })
})
-```
-```dart !!
-// return a redirect response using status 301
-galaxyApi.get("/planets/alderaan", (ctx) async {
- ctx.res.status = 301;
- ctx.res.headers["Location"] = ["https://example.org/debris/alderaan"];
-
- return ctx;
-});
-```
-
-
-
-## API Security
-
-APIs can include security definitions for OIDC-compatible providers such as [Auth0](https://auth0.com/), [FusionAuth](https://fusionauth.io/) and [AWS Cognito](https://aws.amazon.com/cognito/).
-
-
- Applying security at the API allows AWS, Google Cloud and Azure to reject
- unauthenticated or unauthorized requests at the API Gateway, before invoking
- your application code. In serverless environments this reduces costs by
- limiting application load from unwanted or malicious requests.
-
-
-
- Security rules are currently **not enforced** when using nitric for **local**
- development.
-
-
-### Authentication
-
-APIs can be configured to automatically authenticate and allow or reject incoming requests. A `securityDefinitions` object can be provided, which _defines_ the authentication requirements that can later be enforced by the API.
-
-The security definition describes the kind of authentication to perform and the configuration required to perform it. For a [JWT](https://jwt.io) this configuration includes the JWT issuer and audiences.
-
-
- Security definitions only define **available** security requirements for an
- API, they don't automatically **apply** those requirements.
-
-
-Once a security definition is available it can be applied to the entire API or selectively to individual routes.
-
-
-
-```javascript !!
-import { api, oidcRule } from '@nitric/sdk'
-
-const defaultSecurityRule = oidcRule({
- name: 'default',
- audiences: ['https://test-security-definition/'],
- issuer: 'https://dev-abc123.us.auth0.com',
-})
-
-const secureApi = api('main', {
- // apply the security definition to all routes in this API.
- security: [defaultSecurityRule()],
-})
-```
-
-```typescript !!
-import { api, oidcRule } from '@nitric/sdk'
-
-const defaultSecurityRule = oidcRule({
- name: 'default',
- audiences: ['https://test-security-definition/'],
- issuer: 'https://dev-abc123.us.auth0.com',
+// POST /items
+myApi.post('/items', async (ctx) => {
+ const item = ctx.req.json()
+ ctx.res.status = 201
+ ctx.res.json(item)
})
-const secureApi = api('main', {
- // apply the security definition to all routes in this API.
- security: [defaultSecurityRule()],
+// PUT /items/:id
+myApi.put('/items/:id', async (ctx) => {
+ const { id } = ctx.req.params
+ const item = ctx.req.json()
+ ctx.res.json({ id, ...item })
})
```
```python !!
-from nitric.resources import api, ApiOptions, oidc_rule
-from nitric.application import Nitric
+my_api = api('my-api')
-default_security_rule = oidc_rule(
- name="default",
- audiences=["https://test-security-definition/"],
- issuer="https://dev-abc123.us.auth0.com",
-)
+@my_api.get("/items")
+async def list_items(ctx):
+ ctx.res.json({"items": []})
-secure_api = api("main", opts=ApiOptions(
- # apply the security definition to all routes in this API.
- security=[default_security_rule()],
- )
-)
+@my_api.post("/items")
+async def create_item(ctx):
+ item = ctx.req.json
+ ctx.res.status = 201
+ ctx.res.json(item)
-Nitric.run()
+@my_api.put("/items/:id")
+async def update_item(ctx):
+ id = ctx.req.params["id"]
+ item = ctx.req.json
+ ctx.res.json({"id": id, **item})
```
```go !!
-package main
-
-import (
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/apis"
-)
-
-func main() {
- defaultSecurityRule := apis.OidcRule(
- "default",
- "https://dev-abc123.us.auth0.com/.well-known/openid-configuration",
- []string{"https://test-security-definition"},
- )
-
- secureApi := nitric.NewApi(
- "main",
- apis.WithSecurity(defaultSecurityRule([]string{})),
- )
+myApi := nitric.NewApi("my-api")
- nitric.Run()
-}
-```
-
-```dart !!
-import 'package:nitric_sdk/nitric.dart';
-
-// define your security definition
-final defaultSecurityRule = Nitric.oidcRule(
- "default",
- "https://dev-abc123.us.auth0.com",
- ["https://test-security-definition/"]
-);
-
-final secureApi = Nitric.api(
- "main",
- opts: ApiOptions(
- security: [
- // apply the security definition to all routes in this API.
- defaultSecurityRule([])
- ]
- )
-);
-```
-
-
-
-### Authorization
-
-In addition to authentication, Nitric APIs can also be configured to perform authorization based on scopes. Again, this can be done at the top level of the API or on individual routes.
-
-Add the required scopes to the `security` object when applying a security definition.
-
-
-
-```javascript !!
-import { api, oidcRule } from '@nitric/sdk'
-
-const defaultSecurityRule = oidcRule({
- name: 'default',
- audiences: ['https://test-security-definition/'],
- issuer: 'https://dev-abc123.us.auth0.com',
+myApi.Get("/items", func(ctx *apis.Ctx) {
+ ctx.Response.Json(map[string]interface{}{"items": []})
})
-const secureApi = api('main', {
- // apply the security definition to all routes in this API.
- // add scopes to the rule to authorize
- security: [defaultSecurityRule('user.read')],
-})
-```
-
-```typescript !!
-import { api, oidcRule } from '@nitric/sdk'
-
-const defaultSecurityRule = oidcRule({
- name: 'default',
- audiences: ['https://test-security-definition/'],
- issuer: 'https://dev-abc123.us.auth0.com',
+myApi.Post("/items", func(ctx *apis.Ctx) {
+ item := ctx.Request.Json()
+ ctx.Response.Status = 201
+ ctx.Response.Json(item)
})
-const secureApi = api('main', {
- // apply the security definition to all routes in this API.
- // add scopes to the rule to authorize
- security: [defaultSecurityRule('user.read')],
+myApi.Put("/items/:id", func(ctx *apis.Ctx) {
+ id := ctx.Request.PathParams()["id"]
+ item := ctx.Request.Json()
+ ctx.Response.Json(map[string]interface{}{"id": id, "item": item})
})
```
-```python !!
-from nitric.resources import api, ApiOptions, oidc_rule
-from nitric.application import Nitric
-
-default_security_rule = oidc_rule(
- name="default",
- audiences=["https://test-security-definition/"],
- issuer="https://dev-abc123.us.auth0.com",
-)
-
-secure_api = api("main", opts=ApiOptions(
- # apply the security definition to all routes in this API.
- security=[default_security_rule("user.read")],
- )
-)
-
-Nitric.run()
-```
-
-```go !!
-package main
-
-import (
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/apis"
-)
-
-func main() {
- defaultSecurityRule := apis.OidcRule(
- "default",
- "https://dev-abc123.us.auth0.com/.well-known/openid-configuration",
- []string{"https://test-security-definition/"},
- )
-
- secureApi := nitric.NewApi(
- "main",
- apis.WithSecurity(defaultSecurityRule([]string{"user.read"})),
- )
-
- nitric.Run()
-}
-
-```
-
```dart !!
-import 'package:nitric_sdk/nitric.dart';
+final myApi = Nitric.api("my-api");
-// define your security definition
-final defaultSecurityRule = Nitric.oidcRule(
- "default",
- "https://dev-abc123.us.auth0.com",
- ["https://test-security-definition/"]
-);
+myApi.get("/items", (ctx) async {
+ ctx.res.json({"items": []});
+ return ctx;
+});
-final secureApi = Nitric.api(
- "main",
- opts: ApiOptions(
- security: [
- // apply the security definition to all routes in this API.
- defaultSecurityRule(["user.read"])
- ]
- )
-);
+myApi.post("/items", (ctx) async {
+ final item = ctx.req.json;
+ ctx.res.status = 201;
+ ctx.res.json(item);
+ return ctx;
+});
+
+myApi.put("/items/:id", (ctx) async {
+ final id = ctx.req.pathParams["id"];
+ final item = ctx.req.json;
+ ctx.res.json({"id": id, ...item});
+ return ctx;
+});
```
-For an in-depth tutorial look at the [Auth0 integration guide](/guides/nodejs/secure-api-auth0)
-
-### Override API-level security
-
-Individual routes can have their own security rules which apply any available `securityDefinition`. The requirement defined on routes override any requirements previously set at the top level of the API.
+### Path Parameters
-This allows you to selectively increase or decrease the security requirements for specific routes.
+You can define dynamic routes using path parameters:
```javascript !!
-galaxyApi.get('planets/unsecured-planet', async (ctx) => {}, {
- // override top level security to remove security from this route
- security: [],
-})
-
-galaxyApi.post('planets/secured-planet', async (ctx) => {}, {
- // override top level security to require user.write scope to access
- security: [customSecurityRule('user.write')],
+myApi.get('/users/:id', async (ctx) => {
+ const { id } = ctx.req.params
+ ctx.res.json({ id })
})
```
```typescript !!
-galaxyApi.get('planets/unsecured-planet', async (ctx) => {}, {
- // override top level security to remove security from this route
- security: [],
-})
-
-galaxyApi.post('planets/secured-planet', async (ctx) => {}, {
- // override top level security to require user.write scope to access
- security: [customSecurityRule('user.write')],
+myApi.get('/users/:id', async (ctx) => {
+ const { id } = ctx.req.params
+ ctx.res.json({ id })
})
```
```python !!
-# override top level security to remove security from this route
-@galaxy_api.get("planets/unsecured-planet", opts=MethodOptions(security=[]))
-async def get_planet(ctx):
- pass
-
-# override top level security to require user.write scope to access
-@galaxy_api.post("planets/secured-planet", opts=MethodOptions(security=[custom_rule("user.write")]))
-async def get_planet(ctx):
- pass
+@my_api.get("/users/:id")
+async def get_user(ctx):
+ id = ctx.req.params["id"]
+ ctx.res.json({"id": id})
```
```go !!
-// override top level security to remove security from this route
-secureApi.Get("/planets/unsecured-planet", func(ctx *apis.Ctx) {
- // Handle request
-}, apis.WithNoMethodSecurity())
-
-// override top level security to require user.write scope to access
-secureApi.Get("/planets/unsecured-planet", func(ctx *apis.Ctx) {
- // Handle request
-}, apis.WithSecurity(customRule([]string{"users:write"})))
+myApi.Get("/users/:id", func(ctx *apis.Ctx) {
+ id := ctx.Request.PathParams()["id"]
+ ctx.Response.Json(map[string]interface{}{"id": id})
+})
```
```dart !!
-// override top level security to remove security from this route
-galaxyApi.get("/planets/unsecured-planet", (ctx) async {
- return ctx;
-}, security: []);
-
-// override top level security to require user.write scope to access
-galaxyApi.post("/planets/unsecured-planet", (ctx) async {
+myApi.get("/users/:id", (ctx) async {
+ final id = ctx.req.pathParams["id"];
+ ctx.res.json({"id": id});
return ctx;
-}, security: [customRule(["user.write"])]);
+});
```
-## Defining Middleware
+## Advanced Features
-Behavior that's common to several APIs or routes can be applied using middleware. Multiple middleware can also be composed to create a cascading set of steps to perform on incoming requests or outgoing responses.
+### Middleware
-In most of Nitric's supported languages middleware functions look nearly identical to handlers except for an additional parameter called `next`, which is the next middleware or handler to be called in the chain. By providing each middleware the next middleware in the chain it allows them to intercept requests, response and errors to perform operations like logging, decoration, exception handling and many other common tasks.
+Middleware allows you to add common functionality across routes. Each middleware receives the context and a `next` function to continue the request chain:
```javascript !!
-async function validate(ctx, next) {
- if (!ctx.req.headers['content-type']) {
- ctx.res.status = 400
- ctx.res.body = 'header Content-Type is required'
+async function authMiddleware(ctx, next) {
+ const token = ctx.req.headers['authorization']
+ if (!token) {
+ ctx.res.status = 401
return ctx
}
return await next(ctx)
}
+
+const secureApi = api('secure-api', {
+ middleware: [authMiddleware],
+})
```
```typescript !!
-async function validate(ctx, next) {
- if (!ctx.req.headers['content-type']) {
- ctx.res.status = 400
- ctx.res.body = 'header Content-Type is required'
+async function authMiddleware(ctx, next) {
+ const token = ctx.req.headers['authorization']
+ if (!token) {
+ ctx.res.status = 401
return ctx
}
return await next(ctx)
}
+
+const secureApi = api('secure-api', {
+ middleware: [authMiddleware],
+})
```
```python !!
-async def validate(ctx, nxt: HttpMiddleware):
- if ctx.req.headers['content-type'] is None:
- ctx.res.status = 400
- ctx.res.body = "header Content-Type is required"
- return ctx
- return await nxt(ctx)
+async def auth_middleware(ctx, next):
+ token = ctx.req.headers.get("authorization")
+ if not token:
+ ctx.res.status = 401
+ return ctx
+ return await next(ctx)
+
+secure_api = api("secure-api", opts=ApiOptions(middleware=[auth_middleware]))
```
```go !!
-// Using the Go SDK we recommend using higher-order functions to define middleware
-func validate(next apis.Handler) apis.Handler {
- return func (ctx *apis.Ctx) error {
- if ctx.Request.Headers()["content-type"] == nil {
- ctx.Response.Status = 400
- ctx.Response.Body = []byte("header Content-Type is required")
-
- return nil
+func authMiddleware(next apis.Handler) apis.Handler {
+ return func(ctx *apis.Ctx) error {
+ if ctx.Request.Headers()["authorization"] == nil {
+ ctx.Response.Status = 401
+ return nil
+ }
+ return next(ctx)
}
-
- return next(ctx)
- }
}
+
+secureApi := nitric.NewApi("secure-api", apis.WithMiddleware(authMiddleware))
```
```dart !!
-Future validate(HttpContext ctx) async {
- if (!ctx.req.headers.containsKey("Content-Type")) {
- ctx.res.status = 400;
- ctx.res.body = "header Content-Type is required";
-
+Future authMiddleware(HttpContext ctx) async {
+ if (!ctx.req.headers.containsKey("authorization")) {
+ ctx.res.status = 401;
return ctx;
}
-
return ctx.next();
}
+
+final secureApi = Nitric.api(
+ "secure-api",
+ opts: ApiOptions(middlewares: [authMiddleware])
+);
```
-### API level middleware
+### Security
-Middleware defined at the API level will be called on every request to every route.
+APIs can be secured using OIDC-compatible providers like Auth0, FusionAuth, or AWS Cognito. Security can be applied at the API level or per route:
```javascript !!
-import { api } from '@nitric/sdk'
-import { validate, logRequest } from '../middleware'
+import { api, oidcRule } from '@nitric/sdk'
+
+const auth0Rule = oidcRule({
+ name: 'auth0',
+ issuer: 'https://your-tenant.auth0.com',
+ audiences: ['your-api-identifier'],
+})
-const customersApi = api('customers', {
- middleware: [logRequest, validate],
+const secureApi = api('secure-api', {
+ security: [auth0Rule('user.read')],
})
```
```typescript !!
-import { api } from '@nitric/sdk'
-import { validate, logRequest } from '../middleware'
+import { api, oidcRule } from '@nitric/sdk'
-const customersApi = api('customers', {
- middleware: [logRequest, validate],
+const auth0Rule = oidcRule({
+ name: 'auth0',
+ issuer: 'https://your-tenant.auth0.com',
+ audiences: ['your-api-identifier'],
+})
+
+const secureApi = api('secure-api', {
+ security: [auth0Rule('user.read')],
})
```
```python !!
-from nitric.resources import api, ApiOptions
-from common.middleware import validate, log_request
-from nitric.application import Nitric
+from nitric.resources import api, ApiOptions, oidc_rule
-customers_api = api("customers", opts=ApiOptions(middleware=[log_request, validate]))
+auth0_rule = oidc_rule(
+ name="auth0",
+ issuer="https://your-tenant.auth0.com",
+ audiences=["your-api-identifier"]
+)
-Nitric.run()
+secure_api = api("secure-api", opts=ApiOptions(
+ security=[auth0_rule("user.read")]
+))
```
```go !!
-import (
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/apis"
+auth0Rule := apis.OidcRule(
+ "auth0",
+ "https://your-tenant.auth0.com/.well-known/openid-configuration",
+ []string{"your-api-identifier"},
)
-func validate(next apis.Handler) apis.Handler {
- return func(ctx *apis.Ctx) error {
- if ctx.Request.Headers()["content-type"] == nil {
- ctx.Response.Status = 400
- ctx.Response.Body = []byte("header Content-Type is required")
-
- return nil
- }
-
- return next(ctx)
- }
-}
-
-func main() {
- customersApi := nitric.NewApi(
- "customers",
- apis.WithMiddleware(validate))
-
- nitric.Run()
-}
+secureApi := nitric.NewApi(
+ "secure-api",
+ apis.WithSecurity(auth0Rule([]string{"user.read"})),
+)
```
```dart !!
-import 'package:nitric_sdk/nitric.dart';
-import '../middlewares';
+final auth0Rule = Nitric.oidcRule(
+ "auth0",
+ "https://your-tenant.auth0.com",
+ ["your-api-identifier"]
+);
-final customersApi = Nitric.api(
- "customers",
+final secureApi = Nitric.api(
+ "secure-api",
opts: ApiOptions(
- middlewares: [logRequest, validate],
+ security: [auth0Rule(["user.read"])]
)
);
```
-### Route level middleware
-
-Middleware defined at the route level will only be called for that route.
-
-
-
-```javascript !!
-import { api } from '@nitric/sdk'
-import { validate } from '../middleware'
-
-const customersApi = api('customers')
-
-const getAllCustomers = (ctx) => {}
-
-// Inline using .get()
-customersApi.get('/customers', [validate, getAllCustomers])
-
-// Using .route()
-customersApi.route('/customers').get([validate, getAllCustomers])
-```
-
-```typescript !!
-import { api } from '@nitric/sdk'
-import { validate } from '../middleware'
-
-const customersApi = api('customers')
-
-const getAllCustomers = (ctx) => {}
-
-// Inline using .get()
-customersApi.get('/customers', [validate, getAllCustomers])
-
-// Using .route()
-customersApi.route('/customers').get([validate, getAllCustomers])
-```
-
-```python !!
-# Route level middleware currently not supported in python
-```
-
-```go !!
-import (
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/apis"
-)
-
-func validate(next apis.Handler) apis.Handler {
- return func(ctx *apis.Ctx) error {
- if ctx.Request.Headers()["content-type"] == nil {
- ctx.Response.Status = 400
- ctx.Response.Body = []byte("header Content-Type is required")
-
- return nil
- }
-
- return next(ctx)
- }
-}
-
-func main() {
- customersApi := nitric.NewApi("customers")
-
- customersApi.Get("/customers", validate(func(ctx *apis.Ctx) error {
- // handle request
- return nil
- }))
-
- nitric.Run()
-}
-```
-
-```dart !!
-import 'package:nitric_sdk/nitric.dart';
-import '../middlewares';
-
-Future getAllCustomers(HttpContext ctx) async {
- // gets the customers
- return ctx.next();
-}
-
-final customersApi = Nitric.api("customers");
-
-// Inline using .get()
-customersApi.get("/customers", getAllCustomers, middlewares: [logRequest, validate]);
-
-// Inline using .route()
-customersApi.route("/customers", middlewares: [logRequest, validate]).get(getAllCustomers);
-```
-
-
-
-## Custom Domains
-
-Custom domains are currently only supported for AWS deployments.
+### Custom Domains
-By default APIs deployed by Nitric will be assigned a domain by the target cloud provider. If you would like to deploy APIs with predefined custom domains you can specify the custom domains for each API in your project's stack files. For these domains to be successfully configured you will need to meet the prerequisites defined for each cloud provider below.
+You can configure custom domains for your APIs in your stack configuration:
-
+
```yaml title:nitric.prod.yaml
provider: nitric/aws@1.1.0
-region: ap-southeast-2
+region: us-east-1
-# Add a key for configuring apis
apis:
- # Target an API by its nitric name
- my-api-name:
- # provide domains to be used for the api
+ my-api:
domains:
- - test.example.com
+ - api.example.com
```
@@ -885,189 +456,96 @@ apis:
```yaml title:nitric.prod.yaml
-# currently unsupported - request support here: https://github.com/nitrictech/nitric/issues
+# Currently unsupported - request support here: https://github.com/nitrictech/nitric/issues
```
-
+
```yaml title:nitric.prod.yaml
-# currently unsupported - request support here: https://github.com/nitrictech/nitric/issues
+# Currently unsupported - request support here: https://github.com/nitrictech/nitric/issues
```
-## Custom Descriptions
-
-By default, APIs will not be deployed with a description. You can add a description using the following configuration in your stack file.
-
-
-
-
-
-```yaml title:nitric.prod.yaml
-provider: nitric/aws@1.12.4
-region: ap-southeast-2
-
-# Add a key for configuring apis
-apis:
- # Target an API by its nitric name
- my-api-name:
- # provide domains to be used for the api
- description: An AWS API
-```
-
-
-
-
-
-```yaml title:nitric.prod.yaml
-provider: nitric/azure@1.12.4
-region: Australia East
-org: example-org
-adminemail: test@example.com
-
-apis:
- # Target an API by its nitric name
- my-api-name:
- # provide domains to be used for the api
- description: An Azure API
-```
-
-
-
-
-
-```yaml title:nitric.prod.yaml
-provider: nitric/gcp@1.12.4
-region: australia-southeast1
-
-# Add a key for configuring apis
-apis:
- # Target an API by its nitric name
- my-api-name:
- # provide domains to be used for the api
- description: A GCP API
-```
-
-
-
-
-
-### AWS Custom Domain Prerequisites
-
-To support custom domains with APIs deployed to AWS your domain (or subdomain) will need to be setup as a [hosted zone](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/hosted-zones-working-with.html) in Route 53.
-
-The general steps to setup a hosted zone in Route 53 are as follows:
-
-- Navigate to Route 53 in the AWS Console
-- Select 'hosted zones' from the left navigation
-- Click 'Create hosted zone'
-- Enter your domain name and choose the 'Public hosted zone' type.
-- Click 'Create hosted zone'
-- You will now be provided with a set of NS DNS records to configure in the DNS provider for your domain
-- Create the required DNS records, then wait for the DNS changes to propagate
-
-Once this is done you will be able to use the hosted zone domain or any direct subdomain with your Nitric APIs.
-
-You can read more about how AWS suggests configuring hosted zones in their documentation on [Making Route 53 the DNS service for a domain that's in use](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/migrate-dns-domain-in-use.html) or [Making Route 53 the DNS service for an inactive domain](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/migrate-dns-domain-inactive.html)
-
- If the hosted zone was `nitric.io`, `nitric.io` or `api.nitric.io` would be
- supported for APIs, but not `public.api.nitric.io` since that is a subdomain
- of a subdomain.
+ Custom domains are currently only supported for AWS deployments. See the [AWS
+ Custom Domain Setup](/providers/mappings/aws/apis#custom-domain-prerequisites)
+ section for setup details.
-
- DNS propagation of the NS records can take a few seconds to a few hours due to
- the nature of DNS.
-
-
-If you're more of a visual learner, below is a video that walks through how to set up your custom domains.
-
-
-
-## Serving from multiple files
+## Project Organization
-Nitric APIs are scoped to the project and can be referenced from multiple `services`. This allows you to choose the granularity of services that suites your project. For small projects you might have a single service that serves all routes, while in larger projects multiple services might combine to serve paths and methods for your API.
+### Multiple Services
-### Using the same resource name
-
-Since resource names are unique across each Nitric project, you can access a resource in multiple locations by simply reusing it's name. Here's an example of services in different files serving different paths on the same API.
+You can split your API routes across multiple services while using the same API resource:
-```javascript !! title:services/one.js
+```javascript !! title:services/users.js
import { api } from '@nitric/sdk'
-const accountsApi = api('accounts')
+const myApi = api('my-api')
-accountsApi.get('/users/:id', async () => {
- // your logic here
+myApi.get('/users', async (ctx) => {
+ // Handle user listing
})
```
-```typescript !! title:services/one.js
+```typescript !! title:services/users.ts
import { api } from '@nitric/sdk'
-const accountsApi = api('accounts')
+const myApi = api('my-api')
-accountsApi.get('/users/:id', async () => {
- // your logic here
+myApi.get('/users', async (ctx) => {
+ // Handle user listing
})
```
-```python !! title:services/one.py
-from nitric.application import Nitric
-from nitric.context import HttpContext
+```python !! title:services/users.py
from nitric.resources import api
+from nitric.application import Nitric
-accounts_api = api('accounts')
+my_api = api('my-api')
-@accounts_api.get("/users/:id")
-async def get_user(ctx: HttpContext):
- pass # your logic here
+@my_api.get("/users")
+async def list_users(ctx):
+ // Handle user listing
+ pass
Nitric.run()
```
-```go !! title:services/one/main.go
+```go !! title:services/users/main.go
+package main
+
import (
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/apis"
)
func main() {
- accountsApi := nitric.NewApi("accounts")
+ myApi := nitric.NewApi("my-api")
- accountsApi.Get("/users/:id", func(ctx *apis.Ctx) {
- // your logic here
+ myApi.Get("/users", func(ctx *apis.Ctx) {
+ // Handle user listing
})
nitric.Run()
}
```
-```dart !! title:services/one.dart
+```dart !! title:services/users.dart
import 'package:nitric_sdk/nitric.dart';
-final accountsApi = Nitric.api("accounts");
+final myApi = Nitric.api("my-api");
-galaxyApi.get("/users/:id", (ctx) async {
- // your logic here
+myApi.get("/users", (ctx) async {
+ // Handle user listing
+ return ctx;
});
```
@@ -1075,246 +553,186 @@ galaxyApi.get("/users/:id", (ctx) async {
-```javascript !! title:services/two.js
+```javascript !! title:services/products.js
import { api } from '@nitric/sdk'
-const accountsApi = api('accounts')
+const myApi = api('my-api')
-accountsApi.get('/orgs/:id', async () => {
- // your logic here
+myApi.get('/products', async (ctx) => {
+ // Handle product listing
})
```
-```typescript !! title:services/two.ts
+```typescript !! title:services/products.ts
import { api } from '@nitric/sdk'
-const accountsApi = api('accounts')
+const myApi = api('my-api')
-accountsApi.get('/orgs/:id', async () => {
- // your logic here
+myApi.get('/products', async (ctx) => {
+ // Handle product listing
})
```
-```python !! title:services/two.py
-from nitric.application import Nitric
-from nitric.context import HttpContext
+```python !! title:services/products.py
from nitric.resources import api
+from nitric.application import Nitric
-accounts_api = api('accounts')
+my_api = api('my-api')
-@accounts_api.get("/orgs/:id")
-async def get_org(ctx: HttpContext):
- pass # your logic here
+@my_api.get("/products")
+async def list_products(ctx):
+ // Handle product listing
+ pass
Nitric.run()
```
-```go !! title:services/two/main.go
+```go !! title:services/products/main.go
+package main
+
import (
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/apis"
)
func main() {
- accountsApi := nitric.NewApi("accounts")
+ myApi := nitric.NewApi("my-api")
- accountsApi.Get("/orgs/:id", func(ctx *apis.Ctx) {
- // your logic here
+ myApi.Get("/products", func(ctx *apis.Ctx) {
+ // Handle product listing
})
nitric.Run()
}
```
-```dart !! title:services/two.dart
+```dart !! title:services/products.dart
import 'package:nitric_sdk/nitric.dart';
-final accountsApi = Nitric.api("accounts");
+final myApi = Nitric.api("my-api");
-galaxyApi.get("/users/:id", (ctx) async {
- // your logic here
+myApi.get("/products", (ctx) async {
+ // Handle product listing
+ return ctx;
});
```
-
- Calling `api()` multiple times with the same name returns the same API
- resource each time, allowing it to be referenced in multiple services.
-
-
-### Importing an existing resource
-
-While reusing a name is useful, it can lead to errors due to typos or when the configuration of the resource is complex. For this reason it's often preferable to declare the resource in a shared location and import it into the services as needed.
+### Shared Resources
-Here is the same example enhanced to import a common API resource.
+For better organization, you can define shared API resources:
-```javascript !! title:resources/index.js
+```javascript !! title:resources/api.js
import { api } from '@nitric/sdk'
-export const accountsApi = api('accounts')
+export const myApi = api('my-api')
```
-```typescript !! title:resources/index.ts
+```typescript !! title:resources/api.ts
import { api } from '@nitric/sdk'
-export const accountsApi = api('accounts')
+export const myApi = api('my-api')
```
-```python !! title:common/resources.py
+```python !! title:resources/api.py
from nitric.resources import api
-accounts_api = api('accounts')
+my_api = api('my-api')
```
-```go !! title:resources/main.go
+```go !! title:resources/api/main.go
+package api
+
import (
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/apis"
)
-var AccountsApi apis.Api
+var MyApi apis.Api
func init() {
- accountsApi := nitric.NewApi("accounts")
- AccountsApi = accountsApi
+ MyApi = nitric.NewApi("my-api")
}
```
-```dart !! title:common/resources.dart
+```dart !! title:resources/api.dart
import 'package:nitric_sdk/nitric.dart';
-final accountsApi = Nitric.api("accounts");
+final myApi = Nitric.api("my-api");
```
-
-
-```javascript !! title:services/one.js
-import { accountsApi } from '../resources'
-
-accountsApi.get('/users/:id', async () => {
- // your logic here
-})
-```
-
-```typescript !! title:services/one.ts
-import { accountsApi } from '../resources'
-
-accountsApi.get('/users/:id', async () => {
- // your logic here
-})
-```
-
-```python !! title:services/one.py
-from nitric.application import Nitric
-from nitric.context import HttpContext
-from common.resources import accounts_api
-
-
-@accounts_api.get("/users/:id")
-async def get_user(ctx: HttpContext):
- pass # your logic here
-
-Nitric.run()
-```
-
-```go !! title:services/one/main.go
-package main
-
-import (
- "your/resources"
-
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/apis"
-)
-
-func main() {
- resources.AccountsApi.Get("/users/:id", func(ctx *apis.Ctx) {
- // your logic here
- })
-
- nitric.Run()
-}
-```
-
-```dart !! title:services/one.dart
-import '../resources';
-
-accountsApi.get("/users/:id", (ctx) async {
- // your logic here
-});
-```
-
-
+Then import and use them in your services:
-```javascript !! title:services/two.js
-import { accountsApi } from '../resources'
+```javascript !! title:services/users.js
+import { myApi } from '../resources/api'
-accountsApi.get('/orgs/:id', async () => {
- // your logic here
+myApi.get('/users', async (ctx) => {
+ // Handle user listing
})
```
-```typescript !! title:services/two.ts
-import { accountsApi } from '../resources'
+```typescript !! title:services/users.ts
+import { myApi } from '../resources/api'
-accountsApi.get('/orgs/:id', async () => {
- // your logic here
+myApi.get('/users', async (ctx) => {
+ // Handle user listing
})
```
-```python !! title:services/two.py
+```python !! title:services/users.py
from nitric.application import Nitric
-from nitric.context import HttpContext
-from common.resources import accounts_api
-
+from resources.api import my_api
-@accounts_api.get("/orgs/:id")
-async def get_org(ctx: HttpContext):
- pass # your logic here
+@my_api.get("/users")
+async def list_users(ctx):
+ // Handle user listing
+ pass
Nitric.run()
```
-```go !! title:services/two/main.go
+```go !! title:services/users/main.go
package main
import (
- "your/resources"
-
+ "your-project/resources/api"
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/apis"
)
func main() {
- resources.AccountsApi.Get("/orgs/:id", func(ctx *apis.Ctx) {
- // your logic here
+ api.MyApi.Get("/users", func(ctx *apis.Ctx) {
+ // Handle user listing
})
nitric.Run()
}
```
-```dart !! title:services/two.dart
-import '../resources';
+```dart !! title:services/users.dart
+import '../resources/api.dart';
-accountsApi.get("/orgs/:id", (ctx) async {
- // your logic here
+myApi.get("/users", (ctx) async {
+ // Handle user listing
+ return ctx;
});
```
-## Cloud Service Mapping
+## Cloud Provider Support
-Each cloud provider comes with a set of default services used when deploying resources. You can find the default services for each cloud provider below.
+Each cloud provider has specific features and limitations for API deployments:
- [AWS](/providers/mappings/aws/apis)
- [Azure](/providers/mappings/azure/apis)
- [Google Cloud](/providers/mappings/gcp/apis)
+
+Security rules are not enforced during local development.
diff --git a/docs/architecture/websites.mdx b/docs/architecture/websites.mdx
index c954a627f..505ba55c9 100644
--- a/docs/architecture/websites.mdx
+++ b/docs/architecture/websites.mdx
@@ -50,14 +50,14 @@ classDef edgeLabel line-height:2;
flowchart TD
Developer["Developer"]
NitricUp["nitric up"]
- Storage["Azure Storage"]
+ BlobStorage["Azure Blob Storage"]
FrontDoor["Azure Front Door"]
Rewrite["Azure API Management"]
Developer -->|Deploy| NitricUp
- NitricUp -->|Upload Assets| Storage
+ NitricUp -->|Upload Assets| BlobStorage
NitricUp -->|Create CDN| FrontDoor
- FrontDoor -->|Serve Static Files| Storage
+ FrontDoor -->|Serve Static Files| BlobStorage
FrontDoor -->|Rewrite /api/* to API| Rewrite
classDef default line-height:1;
diff --git a/docs/batch.mdx b/docs/batch.mdx
index cd297465d..af29f39c1 100644
--- a/docs/batch.mdx
+++ b/docs/batch.mdx
@@ -1,45 +1,62 @@
---
-description: Running AI & Batch workloads with Nitric
+description: 'Running AI & Batch workloads with Nitric'
---
# Batch
-Nitric provides functionality that allows you to run large-scale jobs in parallel across multiple virtual machines or compute resources. Unlike Nitric Services, which respond to real-time events (APIs, Schedules, etc.), Batch is intended to efficiently handle tasks that can be processed in batches, which means they don't need to run in real time but can be executed asynchronously. Batches can include tasks that require a lot of computing power, or access to GPU resources, such as machine learning model training, image processing, video transcoding, data processing, and data analysis.
+Nitric provides functionality that allows you to run large-scale jobs in parallel across multiple virtual machines or compute resources. Unlike Nitric Services, which respond to real-time events (APIs, Schedules, etc.), Batch is intended to efficiently handle tasks that can be processed in batches, which means they don't need to run in real time but can be executed asynchronously.
- Batches are currently in [Preview](/reference/preview-features) and are
+ Batch services are currently in [Preview](/reference/preview-features) and are
currently only available in the following languages: JavaScript, Python, Go,
and Dart, using the nitric/aws@1.14.0, nitric/gcp@1.14.0 or later.
-Nitric Batch is designed to be used in conjunction with Nitric Services, allowing you to run long-running, computationally intensive tasks in parallel with your real-time services. This allows you to build complex applications that can handle both real-time and batch processing workloads.
+## Overview
-Batches are deployed to cloud services such as [AWS Batch](https://aws.amazon.com/batch/), [Azure Batch](https://azure.microsoft.com/en-au/products/batch), and [Google Cloud Batch](https://cloud.google.com/batch/docs). Nitric abstracts the underlying cloud provider, allowing you to run your batch jobs on any of the supported cloud providers without having to worry about the specifics of each provider.
+Batch is designed for workloads that:
+
+- Require significant computing resources (CPU, memory, GPU)
+- Can be processed asynchronously
+- Need to run in parallel across multiple machines
+- Don't require real-time responses
+
+Common use cases include:
+
+- Machine learning model training
+- Image and video processing
+- Data analysis and transformation
+- Large-scale data processing
+- Scientific computing
## Enabling Batches
-Batches are currently in [Preview](/reference/preview-features). To enable this feature in your project add the following to your `nitric.yaml` file
+Batches are currently in [Preview](/reference/preview-features). To enable this feature in your project add the following to your `nitric.yaml` file:
```yaml
preview:
- batch-services
```
-## Definitions
+## Core Concepts
### Batch
-A Batch is similar to a Nitric Service, but it's intended for work with a definitive start and a finish. Where a service is designed to be reactive, a batch is designed to be proactive and run a series of jobs in parallel.
+A Batch is similar to a Nitric Service, but it's intended for work with a definitive start and finish. Where a service is designed to be reactive, a batch is designed to be proactive and run a series of jobs in parallel.
+
+### Job Definition
-### Job Definitions
+A Job Definition describes a type of work to be done by a Nitric `Batch`. It includes:
-A Job Definition describes a type of work to be done by a Nitric `Batch`
+- The handler function to execute
+- Resource requirements (CPU, memory, GPU)
+- Environment variables and configuration
### Job
-A Job is an instance of a Job Definition that is running within a `Batch`, Jobs can be started from other Nitric Services or Batches.
+A Job is an instance of a Job Definition that is running within a `Batch`. Jobs can be started from other Nitric Services or Batches.
-## Limitations of Batches
+## Limitations
Jobs are designed to be long running HPC workloads and can take some time to spin up. They are not designed with reactivity in mind and are not suitable for responding to events from cloud resources.
@@ -79,6 +96,8 @@ const analyze = job('analyze')
analyze.handler(
async (ctx) => {
// Do some work
+ console.log('Processing job:', ctx.jobName)
+ console.log('Job payload:', ctx.data)
},
{ cpus: 1, memory: 1024, gpus: 0 },
)
@@ -93,6 +112,8 @@ const analyze = job('analyze')
analyze.handler(
async (ctx: JobContext) => {
// Do some work
+ console.log('Processing job:', ctx.jobName)
+ console.log('Job payload:', ctx.data)
},
{ cpus: 1, memory: 1024, gpus: 0 },
)
@@ -105,17 +126,18 @@ from nitric.context import JobContext
analyze = job("analyze")
-# Create the callback function that will run when a job is submitted
+// Create the callback function that will run when a job is submitted
@analyze(cpus=1, memory=1024, gpus=0)
-async def generate_image(ctx: None):
- # Do some work
-
+async def generate_image(ctx: JobContext):
+ print(f"Processing job: {ctx.jobName}")
+ print(f"Job payload: {ctx.data}")
Nitric.run()
```
```go !!
import (
+ "fmt"
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/batch"
)
@@ -125,7 +147,8 @@ func main() {
// Use `Handler` to register the callback function that will run when a job is submitted
analyze.Handler(func(ctx *batch.Ctx) {
- // Do some work
+ fmt.Printf("Processing job: %s\n", ctx.JobName)
+ fmt.Printf("Job payload: %v\n", ctx.Data)
}, batch.WithCpus(1), batch.WithMemory(1024), batch.WithGpus(0))
nitric.Run()
@@ -139,7 +162,8 @@ void main() {
final job = Nitric.job("analyze");
job.handler((ctx) async {
- print("New job submitted for ${ctx.req.jobName}: ${ctx.req.message}");
+ print("Processing job: ${ctx.jobName}");
+ print("Job payload: ${ctx.data}");
return ctx;
}, opts: JobResourceRequirements(cpus: 1, memory: 1024, gpus: 0));
@@ -162,8 +186,9 @@ const analyze = nitric.job('analyze').allow('submit')
api.post('/submit-job', async (ctx) => {
await analyze.submit({
- someKey: 'someValue',
+ data: 'some data to process',
})
+ return ctx
})
```
@@ -175,8 +200,9 @@ const analyze = nitric.job('analyze').allow('submit')
api.post('/submit-job', async (ctx) => {
await analyze.submit({
- someKey: 'someValue',
+ data: 'some data to process',
})
+ return ctx
})
```
@@ -189,35 +215,33 @@ public_api = api("public")
@public_api.post("/submit-job")
async def submit_job(ctx):
- await analyze.submit(
- {
- "someKey": "someValue"
- }
- )
+ await analyze.submit({
+ "data": "some data to process"
+ })
+ return ctx
Nitric.run()
```
```go !!
import (
- "context"
-
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/apis"
- "github.com/nitrictech/go-sdk/nitric/batch"
+ "context"
+ "github.com/nitrictech/go-sdk/nitric"
+ "github.com/nitrictech/go-sdk/nitric/apis"
+ "github.com/nitrictech/go-sdk/nitric/batch"
)
func main() {
- api := nitric.NewApi("public")
- analyze := nitric.NewJob("analyze").Allow(batch.JobSubmit)
+ api := nitric.NewApi("public")
+ analyze := nitric.NewJob("analyze").Allow(batch.JobSubmit)
- api.Post("/submit-job", func(ctx *apis.Ctx) {
- analyze.Submit(context.Background(), map[string]interface{}{
- "someKey": "someValue",
- })
- })
+ api.Post("/submit-job", func(ctx *apis.Ctx) {
+ analyze.Submit(context.Background(), map[string]interface{}{
+ "data": "some data to process"
+ })
+ })
- nitric.Run()
+ nitric.Run()
}
```
@@ -228,11 +252,10 @@ void main() {
final api = Nitric.api("public");
final analyze = Nitric.job("analyze").allow([JobPermission.submit]);
- api.get("/submit-job", (ctx) async {
- analyze.submit({
- "someKey": "someValue"
+ api.post("/submit-job", (ctx) async {
+ await analyze.submit({
+ "data": "some data to process"
});
-
return ctx;
});
}
@@ -240,6 +263,14 @@ void main() {
+## Best Practices
+
+1. **Resource Allocation**: Carefully consider CPU, memory, and GPU requirements for your jobs.
+2. **Error Handling**: Implement robust error handling in job handlers.
+3. **Monitoring**: Set up monitoring for job execution and completion.
+4. **Cost Optimization**: Use appropriate instance types and job durations.
+5. **Job Dependencies**: Consider job dependencies when submitting multiple jobs.
+
## Cloud Service Mapping
Each cloud provider comes with a set of default services used when deploying resources. You can find the default services for each cloud provider below.
diff --git a/docs/guides/async-messaging-patterns.mdx b/docs/guides/async-messaging-patterns.mdx
new file mode 100644
index 000000000..94813e9be
--- /dev/null
+++ b/docs/guides/async-messaging-patterns.mdx
@@ -0,0 +1,899 @@
+---
+description: 'Common patterns for building reliable asynchronous messaging systems with Nitric'
+tags:
+ - Messaging
+ - Topics
+ - Queues
+languages:
+ - typescript
+ - python
+ - go
+ - dart
+published_at: 2025-04-01
+updated_at: 2025-04-01
+---
+
+# Async Messaging Patterns
+
+This guide covers common patterns and best practices for working with asynchronous messaging in Nitric, including topics and queues.
+
+## Message Delivery Guarantees
+
+### At-Least-Once Delivery
+
+Topics and queues in Nitric provide at-least-once delivery guarantees. This means that a message may be delivered more than once, which is important to consider when designing your message processing logic.
+
+### Idempotency
+
+To handle potential duplicate messages, you should design your message processing to be idempotent. Here's an example of how to implement idempotency:
+
+
+
+```javascript !!
+import { queue } from '@nitric/sdk'
+
+const orderQueue = queue('orders').allow('enqueue', 'dequeue')
+
+// Add a unique ID to each message
+await orderQueue.enqueue({
+ id: 'order-123',
+ data: {
+ /* order details */
+ },
+})
+
+// Process messages with idempotency check
+const messages = await orderQueue.dequeue(10)
+for (const message of messages) {
+ const orderId = message.id
+ // Check if order was already processed
+ if (!(await isOrderProcessed(orderId))) {
+ await processOrder(message.data)
+ await markOrderProcessed(orderId)
+ }
+ await message.complete()
+}
+```
+
+```typescript !!
+import { queue } from '@nitric/sdk'
+
+const orderQueue = queue('orders').allow('enqueue', 'dequeue')
+
+// Add a unique ID to each message
+await orderQueue.enqueue({
+ id: 'order-123',
+ data: {
+ /* order details */
+ },
+})
+
+// Process messages with idempotency check
+const messages = await orderQueue.dequeue(10)
+for (const message of messages) {
+ const orderId = message.id
+ // Check if order was already processed
+ if (!(await isOrderProcessed(orderId))) {
+ await processOrder(message.data)
+ await markOrderProcessed(orderId)
+ }
+ await message.complete()
+}
+```
+
+```python !!
+from nitric.resources import queue
+
+order_queue = queue("orders").allow("enqueue", "dequeue")
+
+// Add a unique ID to each message
+await order_queue.enqueue({
+ "id": "order-123",
+ "data": { /* order details */ }
+})
+
+// Process messages with idempotency check
+messages = await order_queue.dequeue(10)
+for message in messages:
+ order_id = message.id
+ // Check if order was already processed
+ if not await is_order_processed(order_id):
+ await process_order(message.data)
+ await mark_order_processed(order_id)
+ await message.complete()
+```
+
+```go !!
+import (
+ "context"
+ "github.com/nitrictech/go-sdk/nitric"
+ "github.com/nitrictech/go-sdk/nitric/queues"
+)
+
+func main() {
+ orderQueue := nitric.NewQueue("orders").Allow(queues.QueueEnqueue, queues.QueueDequeue)
+
+ // Add a unique ID to each message
+ _ = orderQueue.Enqueue(context.TODO(), []map[string]interface{}{
+ {
+ "id": "order-123",
+ "data": map[string]interface{}{ /* order details */ },
+ },
+ })
+
+ // Process messages with idempotency check
+ messages, _ := orderQueue.Dequeue(context.TODO(), 10)
+ for _, message := range messages {
+ orderID := message.Message()["id"].(string)
+ // Check if order was already processed
+ if !isOrderProcessed(context.TODO(), orderID) {
+ processOrder(context.TODO(), message.Message()["data"])
+ markOrderProcessed(context.TODO(), orderID)
+ }
+ message.Complete(context.TODO())
+ }
+}
+```
+
+```dart !!
+import 'package:nitric_sdk/nitric.dart';
+
+final orderQueue = Nitric.queue("orders").allow([
+ QueuePermission.enqueue,
+ QueuePermission.dequeue,
+]);
+
+// Add a unique ID to each message
+await orderQueue.enqueue([{
+ "id": "order-123",
+ "data": { /* order details */ }
+}]);
+
+// Process messages with idempotency check
+final messages = await orderQueue.dequeue(10);
+await Future.wait(messages.map((message) async {
+ final orderId = message.id;
+ // Check if order was already processed
+ if (!await isOrderProcessed(orderId)) {
+ await processOrder(message.data);
+ await markOrderProcessed(orderId);
+ }
+ await message.complete();
+}));
+```
+
+
+
+## Outbox Pattern
+
+The Outbox Pattern is a reliable way to ensure messages are published when they're part of a database transaction. It works by storing messages in a database table (the "outbox") as part of the transaction, then publishing them after the transaction commits.
+
+First, create a migration file for the outbox table:
+
+```sql
+-- migrations/orders/1_create_outbox.up.sql
+CREATE TABLE IF NOT EXISTS outbox (
+ id SERIAL PRIMARY KEY,
+ topic VARCHAR(255) NOT NULL,
+ message JSONB NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+ published_at TIMESTAMP WITH TIME ZONE
+);
+```
+
+Here's an example of implementing the Outbox Pattern with Nitric's SQL resource:
+
+
+
+```javascript !!
+import { sql, topic, api, schedule } from '@nitric/sdk'
+import { Pool } from 'pg'
+
+// Create the database with migrations
+const db = sql('orders', {
+ migrations: 'file://migrations/orders',
+})
+
+// Create the topic for publishing
+const orderTopic = topic('orders').allow('publish')
+
+// Create the API
+const ordersApi = api('orders')
+
+// Function to create an order and publish a message in a transaction
+async function createOrder(orderData) {
+ const connStr = await db.connectionString()
+ const pool = new Pool({ connectionString: connStr })
+
+ const client = await pool.connect()
+ try {
+ await client.query('BEGIN')
+
+ // Insert the order into the orders table
+ await client.query('INSERT INTO orders (data) VALUES ($1) RETURNING id', [
+ orderData,
+ ])
+
+ // Insert the message into the outbox
+ await client.query('INSERT INTO outbox (topic, message) VALUES ($1, $2)', [
+ 'orders',
+ JSON.stringify({
+ type: 'order.created',
+ data: orderData,
+ }),
+ ])
+
+ await client.query('COMMIT')
+ } catch (error) {
+ await client.query('ROLLBACK')
+ throw error
+ } finally {
+ client.release()
+ await pool.end()
+ }
+}
+
+// API endpoint to create orders
+ordersApi.post('/orders', async ({ req, res }) => {
+ try {
+ await createOrder(req.json())
+ res.json({ success: true })
+ } catch (error) {
+ res.status(500).json({ error: error.message })
+ }
+})
+
+// Schedule to publish messages from the outbox
+const publishSchedule = schedule('publish-outbox').every(
+ '1 minute',
+ async () => {
+ const connStr = await db.connectionString()
+ const pool = new Pool({ connectionString: connStr })
+
+ try {
+ const result = await pool.query(`
+ SELECT id, topic, message
+ FROM outbox
+ WHERE published_at IS NULL
+ ORDER BY created_at ASC
+ `)
+
+ for (const msg of result.rows) {
+ try {
+ // Publish the message
+ await orderTopic.publish(msg.message)
+
+ // Mark as published
+ await pool.query(
+ 'UPDATE outbox SET published_at = CURRENT_TIMESTAMP WHERE id = $1',
+ [msg.id],
+ )
+ } catch (error) {
+ console.error('Failed to publish message:', error)
+ // Message will be retried on next run
+ }
+ }
+ } finally {
+ await pool.end()
+ }
+ },
+)
+```
+
+```typescript !!
+import { sql, topic, api, schedule } from '@nitric/sdk'
+import { Pool } from 'pg'
+
+// Create the database with migrations
+const db = sql('orders', {
+ migrations: 'file://migrations/orders',
+})
+
+// Create the topic for publishing
+const orderTopic = topic('orders').allow('publish')
+
+// Create the API
+const ordersApi = api('orders')
+
+// Function to create an order and publish a message in a transaction
+async function createOrder(orderData: OrderData) {
+ const connStr = await db.connectionString()
+ const pool = new Pool({ connectionString: connStr })
+
+ const client = await pool.connect()
+ try {
+ await client.query('BEGIN')
+
+ // Insert the order into the orders table
+ await client.query('INSERT INTO orders (data) VALUES ($1) RETURNING id', [
+ orderData,
+ ])
+
+ // Insert the message into the outbox
+ await client.query('INSERT INTO outbox (topic, message) VALUES ($1, $2)', [
+ 'orders',
+ JSON.stringify({
+ type: 'order.created',
+ data: orderData,
+ }),
+ ])
+
+ await client.query('COMMIT')
+ } catch (error) {
+ await client.query('ROLLBACK')
+ throw error
+ } finally {
+ client.release()
+ await pool.end()
+ }
+}
+
+// API endpoint to create orders
+ordersApi.post('/orders', async ({ req, res }) => {
+ try {
+ await createOrder(req.json())
+ res.json({ success: true })
+ } catch (error) {
+ res.status(500).json({ error: error.message })
+ }
+})
+
+// Schedule to publish messages from the outbox
+const publishSchedule = schedule('publish-outbox').every(
+ '1 minute',
+ async () => {
+ const connStr = await db.connectionString()
+ const pool = new Pool({ connectionString: connStr })
+
+ try {
+ const result = await pool.query(`
+ SELECT id, topic, message
+ FROM outbox
+ WHERE published_at IS NULL
+ ORDER BY created_at ASC
+ `)
+
+ for (const msg of result.rows) {
+ try {
+ // Publish the message
+ await orderTopic.publish(msg.message)
+
+ // Mark as published
+ await pool.query(
+ 'UPDATE outbox SET published_at = CURRENT_TIMESTAMP WHERE id = $1',
+ [msg.id],
+ )
+ } catch (error) {
+ console.error('Failed to publish message:', error)
+ // Message will be retried on next run
+ }
+ }
+ } finally {
+ await pool.end()
+ }
+ },
+)
+```
+
+```python !!
+from nitric.resources import sql, topic, api, schedule
+import psycopg2
+from psycopg2.extras import RealDictCursor
+
+# Create the database with migrations
+db = sql("orders", migrations="file://migrations/orders")
+
+# Create the topic for publishing
+order_topic = topic("orders").allow("publish")
+
+# Create the API
+orders_api = api("orders")
+
+# Function to create an order and publish a message in a transaction
+async def create_order(order_data):
+ conn_str = await db.connection_string()
+ conn = psycopg2.connect(conn_str)
+ try:
+ with conn.cursor() as cur:
+ cur.execute("BEGIN")
+
+ # Insert the order into the orders table
+ cur.execute(
+ "INSERT INTO orders (data) VALUES (%s) RETURNING id",
+ [order_data]
+ )
+
+ # Insert the message into the outbox
+ cur.execute(
+ "INSERT INTO outbox (topic, message) VALUES (%s, %s)",
+ ["orders", {
+ "type": "order.created",
+ "data": order_data
+ }]
+ )
+
+ cur.execute("COMMIT")
+ except Exception as error:
+ cur.execute("ROLLBACK")
+ raise error
+ finally:
+ conn.close()
+
+# API endpoint to create orders
+@orders_api.post("/orders")
+async def handle_create_order(req):
+ try:
+ await create_order(req.json)
+ return {"success": True}
+ except Exception as error:
+ return {"error": str(error)}, 500
+
+# Schedule to publish messages from the outbox
+publish_schedule = schedule("publish-outbox").every("1 minute", async () => {
+ conn_str = await db.connection_string()
+ conn = psycopg2.connect(conn_str, cursor_factory=RealDictCursor)
+ try:
+ with conn.cursor() as cur:
+ cur.execute("""
+ SELECT id, topic, message
+ FROM outbox
+ WHERE published_at IS NULL
+ ORDER BY created_at ASC
+ """)
+
+ for msg in cur.fetchall():
+ try:
+ # Publish the message
+ await order_topic.publish(msg["message"])
+
+ # Mark as published
+ cur.execute(
+ "UPDATE outbox SET published_at = CURRENT_TIMESTAMP WHERE id = %s",
+ [msg["id"]]
+ )
+ except Exception as error:
+ print(f"Failed to publish message: {error}")
+ # Message will be retried on next run
+ finally:
+ conn.close()
+})
+```
+
+```go !!
+import (
+ "context"
+ "database/sql"
+ "encoding/json"
+ _ "github.com/lib/pq"
+ "github.com/nitrictech/go-sdk/nitric"
+ "github.com/nitrictech/go-sdk/nitric/sql"
+ "github.com/nitrictech/go-sdk/nitric/topics"
+ "github.com/nitrictech/go-sdk/nitric/apis"
+ "github.com/nitrictech/go-sdk/nitric/schedules"
+)
+
+func main() {
+ // Create the database with migrations
+ db := nitric.NewSqlDatabase("orders", sql.WithMigrationsPath("file://migrations/orders"))
+
+ // Create the topic for publishing
+ orderTopic := nitric.NewTopic("orders").Allow(topics.TopicPublish)
+
+ // Create the API
+ ordersApi := nitric.NewApi("orders")
+
+ // Function to create an order and publish a message in a transaction
+ func createOrder(ctx context.Context, orderData map[string]interface{}) error {
+ connStr, err := db.ConnectionString(ctx)
+ if err != nil {
+ return err
+ }
+
+ db, err := sql.Open("postgres", connStr)
+ if err != nil {
+ return err
+ }
+ defer db.Close()
+
+ tx, err := db.BeginTx(ctx, nil)
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback()
+
+ // Insert the order into the orders table
+ _, err = tx.ExecContext(ctx,
+ "INSERT INTO orders (data) VALUES ($1) RETURNING id",
+ orderData,
+ )
+ if err != nil {
+ return err
+ }
+
+ // Prepare the message
+ message := map[string]interface{}{
+ "type": "order.created",
+ "data": orderData,
+ }
+ messageJSON, err := json.Marshal(message)
+ if err != nil {
+ return err
+ }
+
+ // Insert the message into the outbox
+ _, err = tx.ExecContext(ctx,
+ "INSERT INTO outbox (topic, message) VALUES ($1, $2)",
+ "orders", messageJSON,
+ )
+ if err != nil {
+ return err
+ }
+
+ return tx.Commit()
+ }
+
+ // API endpoint to create orders
+ ordersApi.Post("/orders", func(ctx context.Context, req *apis.HttpRequest) (*apis.HttpResponse, error) {
+ var orderData map[string]interface{}
+ if err := json.Unmarshal(req.Body, &orderData); err != nil {
+ return &apis.HttpResponse{
+ Status: 400,
+ Body: []byte(`{"error": "Invalid request body"}`),
+ }, nil
+ }
+
+ if err := createOrder(ctx, orderData); err != nil {
+ return &apis.HttpResponse{
+ Status: 500,
+ Body: []byte(`{"error": "` + err.Error() + `"}`),
+ }, nil
+ }
+
+ return &apis.HttpResponse{
+ Status: 200,
+ Body: []byte(`{"success": true}`),
+ }, nil
+ })
+
+ // Schedule to publish messages from the outbox
+ publishSchedule := nitric.NewSchedule("publish-outbox").Every("1 minute", func(ctx context.Context) error {
+ connStr, err := db.ConnectionString(ctx)
+ if err != nil {
+ return err
+ }
+
+ db, err := sql.Open("postgres", connStr)
+ if err != nil {
+ return err
+ }
+ defer db.Close()
+
+ rows, err := db.QueryContext(ctx, `
+ SELECT id, topic, message
+ FROM outbox
+ WHERE published_at IS NULL
+ ORDER BY created_at ASC
+ `)
+ if err != nil {
+ return err
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ var id int
+ var topic string
+ var message []byte
+ if err := rows.Scan(&id, &topic, &message); err != nil {
+ return err
+ }
+
+ try {
+ // Publish the message
+ if err := orderTopic.Publish(ctx, message); err != nil {
+ return err
+ }
+
+ // Mark as published
+ _, err = db.ExecContext(ctx,
+ "UPDATE outbox SET published_at = CURRENT_TIMESTAMP WHERE id = $1",
+ id,
+ )
+ if err != nil {
+ return err
+ }
+ } catch (error) {
+ fmt.Printf("Failed to publish message: %v\n", error)
+ // Message will be retried on next run
+ }
+ }
+ return rows.Err()
+ })
+}
+```
+
+```dart !!
+import 'package:nitric_sdk/nitric.dart';
+import 'package:postgres/postgres.dart';
+
+// Create the database with migrations
+final db = Nitric.sql("orders",
+ migrations: "file://migrations/orders"
+);
+
+// Create the topic for publishing
+final orderTopic = Nitric.topic("orders").allow([
+ TopicPermission.publish,
+]);
+
+// Create the API
+final ordersApi = Nitric.api("orders");
+
+// Function to create an order and publish a message in a transaction
+Future createOrder(Map orderData) async {
+ final connStr = await db.connectionString();
+ final conn = await Connection.open(Uri.parse(connStr));
+
+ try {
+ await conn.execute('BEGIN');
+
+ // Insert the order into the orders table
+ await conn.execute(
+ "INSERT INTO orders (data) VALUES (\$1) RETURNING id",
+ [orderData],
+ );
+
+ // Insert the message into the outbox
+ await conn.execute(
+ "INSERT INTO outbox (topic, message) VALUES (\$1, \$2)",
+ ["orders", {
+ "type": "order.created",
+ "data": orderData,
+ }],
+ );
+
+ await conn.execute('COMMIT');
+ } catch (error) {
+ await conn.execute('ROLLBACK');
+ rethrow;
+ } finally {
+ await conn.close();
+ }
+}
+
+// API endpoint to create orders
+ordersApi.post("/orders", (req) async {
+ try {
+ await createOrder(req.json);
+ return Response.json({"success": true});
+ } catch (error) {
+ return Response.json({"error": error.toString()}, status: 500);
+ }
+});
+
+// Schedule to publish messages from the outbox
+final publishSchedule = Nitric.schedule("publish-outbox").every("1 minute", () async {
+ final connStr = await db.connectionString();
+ final conn = await Connection.open(Uri.parse(connStr));
+
+ try {
+ final results = await conn.execute("""
+ SELECT id, topic, message
+ FROM outbox
+ WHERE published_at IS NULL
+ ORDER BY created_at ASC
+ """);
+
+ for (final row in results) {
+ try {
+ // Publish the message
+ await orderTopic.publish(row[2]);
+
+ // Mark as published
+ await conn.execute(
+ "UPDATE outbox SET published_at = CURRENT_TIMESTAMP WHERE id = \$1",
+ [row[0]],
+ );
+ } catch (error) {
+ print("Failed to publish message: $error");
+ // Message will be retried on next run
+ }
+ }
+ } finally {
+ await conn.close();
+ }
+});
+```
+
+
+
+
+ The Outbox Pattern is particularly useful when you need to ensure that
+ messages are published as part of a database transaction. The background
+ worker is implemented as a Nitric schedule that runs every minute to publish
+ messages from the outbox table.
+
+
+## Error Handling
+
+### Dead Letter Queues
+
+For queues, you can implement a dead letter queue (DLQ) to handle messages that fail processing after multiple attempts:
+
+
+ Dead letter queues may become a native feature of Nitric in the future. If you
+ need this functionality, please let us know by voting or commenting on the
+ [GitHub issue](https://github.com/nitrictech/roadmap/issues/14).
+
+
+
+
+```javascript !!
+import { queue } from '@nitric/sdk'
+
+const mainQueue = queue('orders').allow('enqueue', 'dequeue')
+const dlq = queue('orders-dlq').allow('enqueue')
+
+// Process messages with retry logic
+const messages = await mainQueue.dequeue(10)
+for (const message of messages) {
+ try {
+ await processOrder(message.data)
+ await message.complete()
+ } catch (error) {
+ // Move failed message to DLQ after max retries
+ if (message.retryCount >= 3) {
+ await dlq.enqueue({
+ originalMessage: message.data,
+ error: error.message,
+ })
+ await message.complete()
+ }
+ }
+}
+```
+
+```typescript !!
+import { queue } from '@nitric/sdk'
+
+const mainQueue = queue('orders').allow('enqueue', 'dequeue')
+const dlq = queue('orders-dlq').allow('enqueue')
+
+// Process messages with retry logic
+const messages = await mainQueue.dequeue(10)
+for (const message of messages) {
+ try {
+ await processOrder(message.data)
+ await message.complete()
+ } catch (error) {
+ // Move failed message to DLQ after max retries
+ if (message.retryCount >= 3) {
+ await dlq.enqueue({
+ originalMessage: message.data,
+ error: error.message,
+ })
+ await message.complete()
+ }
+ }
+}
+```
+
+```python !!
+from nitric.resources import queue
+
+main_queue = queue("orders").allow("enqueue", "dequeue")
+dlq = queue("orders-dlq").allow("enqueue")
+
+// Process messages with retry logic
+messages = await main_queue.dequeue(10)
+for message in messages:
+ try:
+ await process_order(message.data)
+ await message.complete()
+ except Exception as error:
+ # Move failed message to DLQ after max retries
+ if message.retry_count >= 3:
+ await dlq.enqueue({
+ "original_message": message.data,
+ "error": str(error)
+ })
+ await message.complete()
+```
+
+```go !!
+import (
+ "context"
+ "github.com/nitrictech/go-sdk/nitric"
+ "github.com/nitrictech/go-sdk/nitric/queues"
+)
+
+func main() {
+ mainQueue := nitric.NewQueue("orders").Allow(queues.QueueEnqueue, queues.QueueDequeue)
+ dlq := nitric.NewQueue("orders-dlq").Allow(queues.QueueEnqueue)
+
+ // Process messages with retry logic
+ messages, _ := mainQueue.Dequeue(context.TODO(), 10)
+ for _, message := range messages {
+ if err := processOrder(context.TODO(), message.Message()["data"]); err != nil {
+ // Move failed message to DLQ after max retries
+ if message.RetryCount() >= 3 {
+ _ = dlq.Enqueue(context.TODO(), []map[string]interface{}{
+ {
+ "original_message": message.Message()["data"],
+ "error": err.Error(),
+ },
+ })
+ }
+ }
+ message.Complete(context.TODO())
+ }
+}
+```
+
+```dart !!
+import 'package:nitric_sdk/nitric.dart';
+
+final mainQueue = Nitric.queue("orders").allow([
+ QueuePermission.enqueue,
+ QueuePermission.dequeue,
+]);
+final dlq = Nitric.queue("orders-dlq").allow([
+ QueuePermission.enqueue,
+]);
+
+// Process messages with retry logic
+final messages = await mainQueue.dequeue(10);
+await Future.wait(messages.map((message) async {
+ try {
+ await processOrder(message.data);
+ await message.complete();
+ } catch (error) {
+ // Move failed message to DLQ after max retries
+ if (message.retryCount >= 3) {
+ await dlq.enqueue([{
+ "original_message": message.data,
+ "error": error.toString()
+ }]);
+ await message.complete();
+ }
+ }
+}));
+```
+
+
+
+## Best Practices
+
+1. **Message Size**: Keep messages small and focused. Large messages can impact performance and increase costs.
+
+2. **Batch Operations**: When possible, use batch operations for better performance:
+
+ - Batch enqueue messages
+ - Process multiple messages in a single dequeue operation
+
+3. **Error Handling**: Implement proper error handling and retry logic:
+
+ - Use dead letter queues for failed messages
+ - Implement exponential backoff for retries
+ - Log errors for monitoring and debugging
+
+4. **Monitoring**: Set up monitoring for your queues and topics:
+
+ - Track message processing times
+ - Monitor error rates
+ - Set up alerts for queue depth
+
+5. **Security**: Follow security best practices:
+
+ - Use minimal required permissions
+ - Encrypt sensitive data in messages
+ - Validate message content
+
+6. **Testing**: Test your message processing logic thoroughly:
+ - Test with duplicate messages
+ - Test error scenarios
+ - Test with different message sizes and types
+
+
+ The implementation details of these patterns may vary depending on your cloud
+ provider. See the provider-specific documentation for more details.
+
diff --git a/docs/http.mdx b/docs/http.mdx
index ae568e257..249f2faf5 100644
--- a/docs/http.mdx
+++ b/docs/http.mdx
@@ -12,9 +12,18 @@ Nitric supports existing web app and API frameworks, including Express, Koa, Fas
https://github.com/nitrictech/nitric/issues
+## Overview
+
+The HTTP resource is particularly useful when you want to:
+
+- Use existing web frameworks without modification
+- Leverage framework-specific features and middleware
+- Maintain compatibility with existing codebases
+- Deploy traditional web applications to the cloud
+
## Connecting existing frameworks
-Here are some examples of how to connect your existing API frameworks with Nitric:
+Here are examples of how to connect popular Node.js frameworks with Nitric:
@@ -53,3 +62,21 @@ http(bootstrap)
+
+## Best Practices
+
+1. **Middleware Order**: Place Nitric's `http()` call after all your middleware and route definitions.
+
+2. **Error Handling**: Use your framework's error handling middleware to ensure consistent error responses.
+
+3. **Environment Variables**: Access environment variables through your framework's configuration system.
+
+4. **Logging**: Use your framework's logging system for consistent application logging.
+
+## Cloud Service Mapping
+
+Each cloud provider comes with a set of default services used when deploying resources. You can find the default services for each cloud provider below.
+
+- [AWS](/providers/mappings/aws/http)
+- [Azure](/providers/mappings/azure/http)
+- [Google Cloud](/providers/mappings/gcp/http)
diff --git a/docs/index.mdx b/docs/index.mdx
index 0ce770c9b..e2d0b8cbe 100644
--- a/docs/index.mdx
+++ b/docs/index.mdx
@@ -27,7 +27,7 @@ const main = api('main')
main.get('/hello/:name', async ({ req, res }) => {
const { name } = req.params
- ctx.res.body = `Hello ${name}`
+ res.body = `Hello ${name}`
})
```
@@ -38,7 +38,7 @@ const main = api('main')
main.get('/hello/:name', async ({ req, res }) => {
const { name } = req.params
- ctx.res.body = `Hello ${name}`
+ res.body = `Hello ${name}`
})
```
@@ -61,6 +61,7 @@ Nitric.run()
// !collapse(1:5) collapsed
import (
"context"
+ "fmt"
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/apis"
diff --git a/docs/keyvalue.mdx b/docs/keyvalue.mdx
index cd6a217ac..e4f9c81ca 100644
--- a/docs/keyvalue.mdx
+++ b/docs/keyvalue.mdx
@@ -1,69 +1,74 @@
---
-description: 'Key value stores'
+description: 'Key value stores with Nitric'
---
# Key/Value
-Nitric provides functionality for provisioning and interacting with persistent key/value stores.
+Nitric provides a simple way to work with key/value stores in your applications. Key/value stores are perfect for storing and retrieving data using unique keys, with fast access times and simple data structures.
-If you're interested in the architecture, provisioning, or deployment steps, they can be found [here](/architecture/keyvalue).
+## Quick Start
-## Definitions
-
-### Key Value Store
-
-Key value stores act as a database which provides the functionality for efficient storage and retrieval of data. The data is fetched via a unique key, which is associated with a value. Due to the uniqueness of the key, accessing the data is incredibly efficient and will occur in constant time as there is no need for complex lookups.
-
-### Key
-
-A key is a unique string that acts as a references to a value. Anytime a value is updated or read from a key value store, it is done so by referencing the key.
-
-### Value
-
-Values are the data that is stored in the key value store. They are referenced by a key and can be any JSON serializable object.
-
-## Creating Key Value Stores
-
-Here's an example of how to create a key value store, with permissions for getting, setting, and deleting:
+Here's a minimal example to get you started:
```javascript !!
import { kv } from '@nitric/sdk'
-const countries = kv('Countries').allow('get', 'set', 'delete')
+const store = kv('my-store').allow('get', 'set')
+
+// Store a value
+await store.set('user-1', { name: 'John', age: 30 })
+
+// Retrieve a value
+const user = await store.get('user-1')
```
```typescript !!
-// You can add typing by specifying the type of values that are stored in the key/value store.
-// This won't validate the values. However, it will provide type completion when interacting with the store.
import { kv } from '@nitric/sdk'
-type Country = {
- name: string
- population: number
-}
+const store = kv('my-store').allow('get', 'set')
+
+// Store a value
+await store.set('user-1', { name: 'John', age: 30 })
-const countries = kv('Countries').allow('get', 'set', 'delete')
+// Retrieve a value
+const user = await store.get('user-1')
```
```python !!
from nitric.resources import kv
from nitric.application import Nitric
-countries = kv('Countries').allow('get', 'set', 'delete')
+store = kv('my-store').allow('get', 'set')
+
+# Store a value
+await store.set('user-1', { "name": "John", "age": 30 })
+
+# Retrieve a value
+user = await store.get('user-1')
Nitric.run()
```
```go !!
import (
+ "context"
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/keyvalue"
)
func main() {
- countries := nitric.NewKv("Countries").Allow(keyvalue.KvStoreGet, keyvalue.KvStoreSet, keyvalue.KvStoreDelete)
+ store := nitric.NewKv("my-store").Allow(keyvalue.KvStoreGet, keyvalue.KvStoreSet)
+
+ // Store a value
+ _ = store.Set(context.TODO(), "user-1", map[string]interface{}{
+ "name": "John",
+ "age": 30,
+ })
+
+ // Retrieve a value
+ user, _ := store.Get(context.TODO(), "user-1")
nitric.Run()
}
@@ -72,378 +77,284 @@ func main() {
```dart !!
import 'package:nitric_sdk/nitric.dart';
-final countries = Nitric.kv("Countries").allow([
+final store = Nitric.kv("my-store").allow([
KeyValueStorePermission.get,
KeyValueStorePermission.set,
- KeyValueStorePermission.delete,
]);
-```
-
-
-
-## Creating Key Value Pairs
-Key value pairs are created based on the `key` and the contents of the `value`. If a key already exists in the key value store, then its value will be overwritten.
-
-> Key value stores that are created using the Nitric SDK are compatible across cloud providers.
-
-The below example first creates a key value store that has permissions for setting. It then adds a key to the store called `USA` and a value which describes the country.
-
-
-
-```javascript !!
-import { kv } from '@nitric/sdk'
-
-const countries = kv('Countries').allow('set')
-
-await countries.set('USA', {
- name: 'United States of America',
- population: 329500000,
-})
-```
-
-```typescript !!
-import { kv } from '@nitric/sdk'
-
-const countries = kv('Countries').allow('set')
+// Store a value
+await store.set("user-1", {
+ "name": "John",
+ "age": 30
+});
-await countries.set('USA', {
- name: 'United States of America',
- population: 329500000,
-})
+// Retrieve a value
+final user = await store.get("user-1");
```
-```python !!
-from nitric.resources import kv
-from nitric.application import Nitric
-
-countries = kv('Countries').allow('set')
-
-await countries.set('USA', {
- "name": "United States of America",
- "population": 329500000
-})
+
-Nitric.run()
-```
+## Core Concepts
-```go !!
-import (
- "context"
+### Key/Value Store
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/keyvalue"
-)
+A key/value store is a database that stores data as key-value pairs. Each value is accessed using a unique key, making lookups fast and efficient.
-func main() {
- countries := nitric.NewKv("Countries").Allow(keyvalue.KvStoreSet)
+### Permissions
- _ = countries.Set(context.TODO(), "USA", map[string]interface{}{
- "name": "United States of America",
- "population": 329500000,
- })
+Key/value stores require explicit permissions for operations:
- nitric.Run()
-}
-```
+- `get`: Read values or list keys
+- `set`: Create or update values
+- `delete`: Remove values
-```dart !!
-import 'package:nitric_sdk/nitric.dart';
+### Keys and Values
-final countries = Nitric.kv("Countries").allow([
- KeyValueStorePermission.set,
-]);
+- **Keys**: Unique strings that identify values
+- **Values**: Any JSON-serializable data (objects, arrays, strings, numbers, etc.)
-await countries.set("USA", {
- "name": "United States of America",
- "population": 329500000
-});
-```
+## Common Operations
-
-
-To update a value call `set` with the same key and a new value.
+### Setting Values
```javascript !!
-await countries.set('USA', {
- name: 'United States of America',
- population: 330000000,
-})
+const store = kv('my-store').allow('set')
+
+// Create or update a value
+await store.set('user-1', { name: 'John', age: 30 })
```
```typescript !!
-await countries.set('USA', {
- name: 'United States of America',
- population: 330000000,
-})
+const store = kv('my-store').allow('set')
+
+// Create or update a value
+await store.set('user-1', { name: 'John', age: 30 })
```
```python !!
-await countries.set('USA', {
- "name": "United States of America",
- "population": 330000000,
-})
+store = kv('my-store').allow('set')
+
+// Create or update a value
+await store.set('user-1', { "name": "John", "age": 30 })
```
```go !!
-_ = countries.Set(context.TODO(), "USA", map[string]interface{}{
- "name": "United States of America",
- "population": 329500000,
+store := nitric.NewKv("my-store").Allow(keyvalue.KvStoreSet)
+
+// Create or update a value
+_ = store.Set(context.TODO(), "user-1", map[string]interface{}{
+ "name": "John",
+ "age": 30,
})
```
```dart !!
-await countries.set("USA", {
- "name": "United States of America",
- "population": 330000000
+final store = Nitric.kv("my-store").allow([
+ KeyValueStorePermission.set,
+]);
+
+// Create or update a value
+await store.set("user-1", {
+ "name": "John",
+ "age": 30
});
```
-## Accessing Values
-
-Values can be accessed from a store by calling the `get` method with the key of the value you want to retrieve.
+### Getting Values
```javascript !!
-import { kv } from '@nitric/sdk'
+const store = kv('my-store').allow('get')
-const countries = kv('Countries').allow('get')
-
-const country = await country.get('USA')
+const user = await store.get('user-1')
```
```typescript !!
-import { kv } from '@nitric/sdk'
-
-const countries = kv('Countries').allow('get')
+const store = kv('my-store').allow('get')
-const country = await country.get('USA')
+const user = await store.get('user-1')
```
```python !!
-from nitric.resources import kv
-from nitric.application import Nitric
-
-countries = kv("Countries").allow("get")
+store = kv('my-store').allow('get')
-country = await countries.get("USA")
-
-Nitric.run()
+user = await store.get('user-1')
```
```go !!
-import (
- "context"
+store := nitric.NewKv("my-store").Allow(keyvalue.KvStoreGet)
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/keyvalue"
-)
-
-func main() {
- countries := nitric.NewKv("Countries").Allow(keyvalue.KvStoreGet)
-
- country, _ := countries.Get(context.TODO(), "USA")
-
- nitric.Run()
-}
+user, _ := store.Get(context.TODO(), "user-1")
```
```dart !!
-import 'package:nitric_sdk/nitric.dart';
-
-final countries = Nitric.kv("Countries").allow([
+final store = Nitric.kv("my-store").allow([
KeyValueStorePermission.get,
]);
-final country = await countries.get('USA');
+final user = await store.get('user-1');
```
-## Deleting Values
-
-Values can be deleted from the key value store by referencing the key.
-
-The below example first creates a key value store that has permissions for deleting and setting. It then creates a value called `USA`, which is deleted using `delete` on the key.
+### Deleting Values
```javascript !!
-import { kv } from '@nitric/sdk'
-
-const countries = kv('Countries').allow('delete', 'set')
-
-await countries.set('USA', {
- name: 'United States of America',
- population: 329500000,
-})
+const store = kv('my-store').allow('delete')
-await countries.delete('USA')
+await store.delete('user-1')
```
```typescript !!
-import { kv } from '@nitric/sdk'
-
-const countries = kv('Countries').allow('delete', 'set')
+const store = kv('my-store').allow('delete')
-await countries.set('USA', {
- name: 'United States of America',
- population: 329500000,
-})
-
-await countries.delete('USA')
+await store.delete('user-1')
```
```python !!
-from nitric.resources import kv
-from nitric.application import Nitric
-
-countries = kv('Countries').allow('set', 'delete')
+store = kv('my-store').allow('delete')
-await countries.set('USA', {
- "name": "United States of America",
- "population": 329500000
-})
-
-await countries.delete('USA')
-
-Nitric.run()
+await store.delete('user-1')
```
```go !!
-import (
- "context"
-
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/keyvalue"
-)
-
-func main() {
- countries := nitric.NewKv("Countries").Allow(keyvalue.KvStoreSet, keyvalue.KvStoreDelete)
-
- _ = countries.Set(context.TODO(), "USA", map[string]interface{}{
- "name": "United States of America",
- "population": 329500000,
- })
+store := nitric.NewKv("my-store").Allow(keyvalue.KvStoreDelete)
- _ = countries.Delete(context.TODO(), "USA")
-
- nitric.Run()
-}
+_ = store.Delete(context.TODO(), "user-1")
```
```dart !!
-import 'package:nitric_sdk/nitric.dart';
-
-final countries = Nitric.kv("Countries").allow([
- KeyValueStorePermission.set,
+final store = Nitric.kv("my-store").allow([
KeyValueStorePermission.delete,
]);
-await countries.set("USA", {
- "name": "United States of America",
- "population": 329500000
-});
-
-await countries.delete("USA")
+await store.delete("user-1");
```
-## Listing Keys
-
-Keys can be listed and filtered by an optional prefix.
-
-The below example first creates a key value store that has permissions for listing. It then lists all keys in the store.
+### Listing Keys
```javascript !!
-import { kv } from '@nitric/sdk'
+const store = kv('my-store').allow('get')
-const countries = kv('Countries').allow('get')
+// List all keys
+for await (const key of store.keys()) {
+ console.log(key)
+}
-for await (const key of countries.keys()) {
+// List keys with prefix
+for await (const key of store.keys('user-')) {
console.log(key)
}
```
```typescript !!
-import { kv } from '@nitric/sdk'
+const store = kv('my-store').allow('get')
-const countries = kv('Countries').allow('get')
+// List all keys
+for await (const key of store.keys()) {
+ console.log(key)
+}
-for await (const key of countries.keys()) {
+// List keys with prefix
+for await (const key of store.keys('user-')) {
console.log(key)
}
```
```python !!
-from nitric.resources import kv
-from nitric.application import Nitric
-
-countries = kv('Countries').allow('get')
+store = kv('my-store').allow('get')
-async for key in countries.keys():
+# List all keys
+async for key in store.keys():
print(key)
-Nitric.run()
+# List keys with prefix
+async for key in store.keys('user-'):
+ print(key)
```
```go !!
-import (
- "context"
- "fmt"
-
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/keyvalue"
-)
-
-func main() {
-
- countries := nitric.NewKv("Countries").Allow(keyvalue.KvStoreGet)
-
- keys, _ := countries.Keys(context.TODO())
-
- for {
- key, err := keys.Recv()
- if err != nil {
- // check if the stream has ended
- break
- }
-
- fmt.Println(key)
+store := nitric.NewKv("my-store").Allow(keyvalue.KvStoreGet)
+
+// List all keys
+keys, _ := store.Keys(context.TODO())
+for {
+ key, err := keys.Recv()
+ if err != nil {
+ break
}
+ fmt.Println(key)
+}
- nitric.Run()
+// List keys with prefix
+keys, _ := store.Keys(context.TODO(), keyvalue.WithPrefix("user-"))
+for {
+ key, err := keys.Recv()
+ if err != nil {
+ break
+ }
+ fmt.Println(key)
}
```
```dart !!
-import 'package:nitric_sdk/nitric.dart';
-
-final countries = Nitric.kv("Countries").allow([
+final store = Nitric.kv("my-store").allow([
KeyValueStorePermission.get,
]);
-final keys = await countries.keys();
-
+// List all keys
+final keys = await store.keys();
keys.forEach((String key) {
print(key);
});
+
+// List keys with prefix
+final userKeys = await store.keys(prefix: "user-");
+userKeys.forEach((String key) {
+ print(key);
+});
```
-## Cloud Service Mapping
+## Cloud Provider Support
Each cloud provider comes with a set of default services used when deploying resources. You can find the default services for each cloud provider below.
-- [AWS](/providers/mappings/aws/keyvalue)
-- [Azure](/providers/mappings/azure/keyvalue)
-- [Google Cloud](/providers/mappings/gcp/keyvalue)
+
+
+
+
+Key value stores are mapped to DynamoDB in AWS. For more details about the AWS implementation, including configuration options and limitations, see the [AWS Key/Value documentation](/providers/mappings/aws/keyvalue).
+
+
+
+
+
+Key value stores are mapped to Azure Cosmos DB in Azure. For more details about the Azure implementation, including configuration options and limitations, see the [Azure Key/Value documentation](/providers/mappings/azure/keyvalue).
+
+
+
+
+
+Key value stores are mapped to Firestore in Google Cloud. For more details about the GCP implementation, including configuration options and limitations, see the [Google Cloud Key/Value documentation](/providers/mappings/gcp/keyvalue).
+
+
+
+
+
+
+ When running locally, key value stores use a local implementation for
+ development and testing.
+
diff --git a/docs/messaging.mdx b/docs/messaging.mdx
deleted file mode 100644
index 9bd24f0e2..000000000
--- a/docs/messaging.mdx
+++ /dev/null
@@ -1,724 +0,0 @@
----
-description: 'Using Queues and Topics for async messaging with Nitric'
----
-
-# Messaging
-
-Nitric provides two common options for scalable, decoupled, asynchronous messaging between services. Topics for publish/subscribe messaging, where new messages are immediately _pushed_ to subscribers, and Queues for _pull_ messaging where new messages are put on a queue and must be requested.
-
-In some circumstances messages sent to a _Topic_ may also be called _Events_, while messages sent to a _Queue_ may be called _Tasks_. The structures of these messages are very similar, but the delivery and retry mechanisms are different. It can be helpful to refer to these messages differently to assist in understanding the context in which they are used.
-
-## Topics
-
-A topic is a named resource where events can be published. They can be thought of as a subject that your services are communicating about.
-
-Topics are often the first choice for communication between services, since they offer stateless, scalable and highly decoupled communication.
-
-If you're interested in the architecture, provisioning, or deployment steps, they can be found [here](/architecture/topics).
-
-### Subscriptions
-
-A subscription is a binding between a topic and a service. You can think of it as a channel that notifies your services when something new arrives on the topic.
-
-### Creating a Topic
-
-Nitric allows you to define named topics. When defining topics, you can give the service permissions for publishing. If permissions are not specified, subscribers can be created.
-
-Here's an example of how to create a topic with permissions to publish messages.
-
-
-
-```javascript !!
-import { topic } from '@nitric/sdk'
-
-const userCreatedTopic = topic('user-created').allow('publish')
-```
-
-```typescript !!
-import { topic } from '@nitric/sdk'
-
-const userCreatedTopic = topic('user-created').allow('publish')
-```
-
-```python !!
-from nitric.resources import topic
-from nitric.application import Nitric
-
-user_created_topic = topic("user-created").allow("publish")
-
-Nitric.run()
-```
-
-```go !!
-import (
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/topics"
-)
-
-func main() {
- userCreatedTopic := nitric.NewTopic("user-created").Allow(topics.TopicPublish)
-
- nitric.Run()
-}
-```
-
-```dart !!
-import 'package:nitric_sdk/nitric.dart';
-
-final userCreatedTopic = Nitric.topic("user-created").allow([
- TopicPermission.publish,
-]);
-```
-
-
-
-### Publishing a message
-
-To send a message to a topic and notify all subscribers, use the `publish()` method on the topic reference. The service must have permissions to `publish` to the topic.
-
-The below example publishes a message to a topic called `user-created`.
-
-
-
-```javascript !!
-import { topic } from '@nitric/sdk'
-
-const userCreatedTopic = topic('user-created').allow('publish')
-
-await userCreatedTopic.publish({
- email: 'new.user@example.com',
-})
-```
-
-```typescript !!
-import { topic } from '@nitric/sdk'
-
-const userCreatedTopic = topic('user-created').allow('publish')
-
-await userCreatedTopic.publish({
- email: 'new.user@example.com',
-})
-```
-
-```python !!
-from nitric.resources import topic
-from nitric.application import Nitric
-
-user_created_topic = topic("user-created").allow("publish")
-
-await user_created_topic.publish({
- "email": "new.user@example.com"
-})
-
-Nitric.run()
-```
-
-```go !!
-import (
- "context"
-
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/topics"
-)
-
-func main() {
- userCreatedTopic := nitric.NewTopic("user-created").Allow(topics.TopicPublish)
-
- _ = userCreatedTopic.Publish(context.TODO(), map[string]interface{}{
- "email": "new.user@example.com",
- })
-
- nitric.Run()
-}
-```
-
-```dart !!
-import 'package:nitric_sdk/nitric.dart';
-
-final userCreatedTopic = Nitric.topic("user-created").allow([
- TopicPermission.publish,
-]);
-
-await userCreatedTopic.publish({
- "email": "new.user@example.com",
-});
-```
-
-
-
-### Subscribing to a topic
-
-To execute a function when new messages are published you can create subscribers. The delay between publishing a message and a subscriber being executed is usually only a few milliseconds. This makes subscribers perfect for responding to messages as they happen.
-
-The below code shows a subscription that responds to messages when new users are created.
-
-
-
-```javascript !!
-import { topic } from '@nitric/sdk'
-import { sendWelcomeEmail } from 'common'
-
-const userCreatedTopic = topic('user-created')
-
-userCreatedTopic.subscribe(async (ctx) => {
- // Extract data from the event payload for processing
- const { email } = ctx.req.json()
-
- sendWelcomeEmail(email)
-})
-```
-
-```typescript !!
-import { topic } from '@nitric/sdk'
-import { sendWelcomeEmail } from 'common'
-
-const userCreatedTopic = topic('user-created')
-
-userCreatedTopic.subscribe(async (ctx) => {
- // Extract data from the event payload for processing
- const { email } = ctx.req.json()
-
- sendWelcomeEmail(email)
-})
-```
-
-```python !!
-from nitric.resources import topic
-from nitric.application import Nitric
-from common import send_welcome_email
-
-user_created_topic = topic("user-created")
-
-@user_created_topic.subscribe()
-async def updates_sub(ctx):
- email = ctx.req.data['email']
-
- send_welcome_email(email)
-
-Nitric.run()
-```
-
-```go !!
-import (
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/topics"
-)
-
-func main() {
- userCreatedTopic := nitric.NewTopic("user-created")
-
- userCreatedTopic.Subscribe(func(ctx *topics.Ctx) {
- email := ctx.Request.Message()["email"].(string)
-
- sendWelcomeEmail(email)
- })
-
- nitric.Run()
-}
-
-func sendWelcomeEmail(email string) {
- // TODO: Implement email sending
-}
-```
-
-```dart !!
-import 'package:nitric_sdk/nitric.dart';
-import 'package:common';
-
-final userCreatedTopic = Nitric.topic("user-created");
-
-userCreatedTopic.subscribe((ctx) async {
- final email = ctx.req.message["email"];
-
- sendWelcomeEmail(email);
-
- return ctx;
-});
-```
-
-
-
-### Reliable subscribers
-
-If a subscriber encounters an error or is terminated before it finishes processing a message, what happens? Is the event lost?
-
-Nitric deploys topics to cloud services that support "at-least-once delivery". Messages are _usually_ delivered exactly once, in the same order that they're published. However, to prevent lost messages, they're sometimes delivered more than once or out of order.
-
-Typically, retries occur when a subscriber doesn't respond successfully, like when unhandled exceptions occur. You'll typically want to ensure messages aren't processed again by accident or partially processed, leaving the system in an unexpected state.
-
-Building atomic publishers and idempotent subscribers can solve this.
-
-#### Atomic publishers
-
-Your publishers need to update your database _and_ publish associated events. If a database update fails, the events should _never_ be sent. If the database update succeeds, the events should _always_ publish. The two shouldn't occur independently (i.e. one shouldn't fail while the other succeeds).
-
-One solution to this problem is the [Transactional Outbox Pattern](https://microservices.io/patterns/data/transactional-outbox.html).
-
-#### Idempotent subscribers
-
-Messages from a topic can be delivered more than once, but they should typically only be _processed_ once. To do this your subscribers need to identify and disregard duplicate events.
-
-Usually checking for duplicate payloads or IDs is enough. When you receive an event you've seen before don't process it, skip straight to returning a `success` response from your subscriber.
-
-
-
-```javascript !!
-import { topic } from '@nitric/sdk'
-import { isDuplicate } from '../common'
-
-const updates = topic('updates')
-
-updates.subscribe((ctx) => {
- if (isDuplicate(ctx.req)) {
- return ctx
- }
- // not a duplicate, process the event
- // ...
-})
-```
-
-```typescript !!
-import { topic } from '@nitric/sdk'
-import { isDuplicate } from '../common'
-
-const updates = topic('updates')
-
-updates.subscribe((ctx) => {
- if (isDuplicate(ctx.req)) {
- return ctx
- }
- // not a duplicate, process the event
- // ...
-})
-```
-
-```python !!
-from nitric.resources import topic
-from nitric.application import Nitric
-from common import is_duplicate
-
-updates = topic("updates")
-
-@updates.subscribe()
-async def process_update(ctx):
- if is_duplicate(ctx.req):
- return ctx
-
- # not a duplicate, process the event
- # ...
-
-Nitric.run()
-```
-
-```go !!
-import (
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/topics"
- "github.com/your-org/your-project/common"
-)
-
-func main() {
- userCreatedTopic := nitric.NewTopic("updates")
-
- userCreatedTopic.Subscribe(func(ctx *topics.Ctx) {
- if common.isDuplicate(ctx.Request) {
- return
- }
-
- // not a duplicate, process the event
- // ...
- })
-
- nitric.Run()
-}
-```
-
-```dart !!
-import 'package:nitric_sdk/nitric.dart';
-import 'package:common';
-
-final updates = Nitric.topic("updates");
-
-updates.subscribe((ctx) async {
- if isDuplicate(ctx.req) {
- return ctx;
- }
-
- // Not a duplicate, process the event
- // ...
-
- return ctx;
-});
-```
-
-
-
-
- If you're checking for duplicate IDs, ensure publishers can't resend failed
- events with new IDs.
-
-
-You can read more about idempotent subscribers and patterns to handle it [here](https://microservices.io/post/microservices/patterns/2020/10/16/idempotent-consumer.html).
-
-## Queues
-
-Queues are another option for asynchronous messaging. Unlike [topics](#topics), messages sent to a queue won't automatically trigger services to process them. Instead, services dequeue message by requesting them.
-
-This makes queues ideal for batch workloads, often paired with [schedules](/schedules).
-
-If you're interested in the architecture, provisioning, or deployment steps, they can be found [here](/architecture/queues).
-
-
- Queue disambiguation: Some systems and cloud providers use the term "queue" to
- refer to a messaging broker with features akin to a topic (i.e. they can push
- messages to subscribers). In Nitric, a queue is a resource that messages are
- sent to and pulled from, while a topic is a resource that messages are sent to
- and pushed from.
-
-
-### Creating a Queue
-
-Nitric allows you to define named queues. When defining queues, you can give the service permissions for enqueueing and dequeueing messages.
-
-Here's an example of how to create a queue with permissions for enqueueing and dequeueing.
-
-
-
-```javascript !!
-import { queue } from '@nitric/sdk'
-
-const transactionQueue = queue('transactions').allow('enqueue', 'dequeue')
-```
-
-```typescript !!
-import { queue } from '@nitric/sdk'
-
-const transactionQueue = queue('transactions').allow('enqueue', 'dequeue')
-```
-
-```python !!
-from nitric.resources import queue
-
-transaction_queue = queue("transactions").allow("enqueue", "dequeue")
-
-Nitric.run()
-```
-
-```go !!
-import (
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/queues"
-)
-
-func main() {
- transactionQueue := nitric.NewQueue("transactions").Allow(queues.QueueEnqueue, queues.QueueDequeue)
-
- nitric.Run()
-}
-```
-
-```dart !!
-import 'package:nitric_sdk/nitric.dart';
-
-final transactionQueue = Nitric.queue("transactions").allow([
- QueuePermission.enqueue,
- QueuePermission.dequeue,
-]);
-```
-
-
-
-### Enqueue Messages
-
-To send a message to a queue, use the `enqueue()` method on the queue reference. The function must have permissions to `enqueue` to the queue.
-
-The below example sends a message to a queue called `transactions`.
-
-
-
-```javascript !!
-import { queue } from '@nitric/sdk'
-
-const transactionQueue = queue('transactions').allow('enqueue')
-
-await transactionQueue.enqueue({
- message: 'hello world',
-})
-```
-
-```typescript !!
-import { queue } from '@nitric/sdk'
-
-const transactionQueue = queue('transactions').allow('enqueue')
-
-await transactionQueue.enqueue({
- message: 'hello world',
-})
-```
-
-```python !!
-from nitric.resources import queue
-
-transaction_queue = queue("transactions").allow("enqueue")
-
-await transaction_queue.enqueue({
- "message": "hello world"
-})
-
-Nitric.run()
-```
-
-```go !!
-import (
- "context"
-
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/queues"
-)
-
-func main() {
- transactionQueue := nitric.NewQueue("transactions").Allow(queues.QueueEnqueue)
-
- messages := []map[string]interface{}{
- {"message": "hello world"},
- }
-
- transactionQueue.Enqueue(context.TODO(), messages)
-
- nitric.Run()
-}
-```
-
-```dart !!
-import 'package:nitric_sdk/nitric.dart';
-
-final transactionQueue = Nitric.queue("transactions").allow([
- QueuePermission.enqueue,
-]);
-
-final messages = [{ "message": "hello world" }];
-
-await transactionQueue.enqueue(messages);
-```
-
-
-
-Messages can also be sent in batches by providing an array.
-
-
-
-```javascript !!
-import { queue } from '@nitric/sdk'
-
-const transactionQueue = queue('transactions').allow('enqueue')
-
-await transactionQueue.enqueue([
- {
- message: 'batch task 1',
- },
- {
- message: 'batch task 2',
- },
-])
-```
-
-```typescript !!
-import { queue } from '@nitric/sdk'
-
-const transactionQueue = queue('transactions').allow('enqueue')
-
-await transactionQueue.enqueue([
- {
- message: 'batch task 1',
- },
- {
- message: 'batch task 2',
- },
-])
-```
-
-```python !!
-from nitric.resources import queue
-from nitric.application import Nitric
-
-transaction_queue = queue("transactions").allow("enqueue")
-
-await transaction_queue.enqueue([
- {
- "message": "batch task 1"
- }, {
- "message": "batch task 2"
- }
-])
-
-Nitric.run()
-```
-
-```go !!
-import (
- "context"
-
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/queues"
-)
-
-func main() {
- transactionQueue := nitric.NewQueue("transactions").Allow(queues.QueueEnqueue)
-
- messages := []map[string]interface{}{
- {"message": "batch task 1"},
- {"message": "batch task 2"},
- }
-
- transactionQueue.Enqueue(context.TODO(), messages)
-
- nitric.Run()
-}
-```
-
-```dart !!
-import 'package:nitric_sdk/nitric.dart';
-
-final transactionQueue = Nitric.queue("transactions").allow([
- QueuePermission.enqueue,
-]);
-
-await transactionQueue.enqueue([
- {
- "message": "batch task 1"
- }, {
- "message": "batch task 2"
- }
-]);
-```
-
-
-
-### Dequeueing and Acknowledging Messages
-
-When you dequeue messages they are not immediately deleted from the queue. Instead, they are leased, which means they are temporarily hidden from other services until the lease expires.
-
-To ensure proper handling, your code should mark a dequeued message as `complete` after successfully processing it. This action permanently removes the message from the queue.
-
-If a lease expires before a dequeued message is marked as complete, the message will reappear in the queue and can be dequeued again. This mechanism prevents messages from getting lost in case of failures. If your service encounters an error or is terminated before completing processing of a dequeued message, it will automatically reappear in the queue, ready to be processed again.
-
-By following this approach, you can ensure reliable message processing and minimize the chances of losing data in failure scenarios.
-
-The below example dequeues 10 messages using the `dequeue()` method, then acknowledges them as complete using the `complete()` method. You'll note that the service has permissions to dequeue messages.
-
-
-
-```javascript !!
-import { queue } from '@nitric/sdk'
-
-const transactionQueue = queue('transactions').allow('dequeue')
-
-const tasks = await transactionQueue.dequeue(10)
-
-for (let task of tasks) {
- // process your task's data
- console.log(task.message)
-
- // acknowledge when the task is complete
- await task.complete()
-}
-```
-
-```typescript !!
-import { queue } from '@nitric/sdk'
-
-const transactionQueue = queue('transactions').allow('dequeue')
-
-const tasks = await transactionQueue.dequeue(10)
-
-for (let task of tasks) {
- // process your task's data
- console.log(task.message)
-
- // acknowledge when the task is complete
- await task.complete()
-}
-```
-
-```python !!
-from nitric.resources import queue
-
-transaction_queue = queue("transactions").allow("dequeue")
-
-tasks = await transaction_queue.dequeue(10)
-
-for task in tasks:
- # process your task's data
- print(task.message)
-
- # acknowledge when the task is complete
- await task.complete()
-```
-
-```go !!
-import (
- "context"
- "fmt"
-
- "github.com/nitrictech/go-sdk/nitric"
- "github.com/nitrictech/go-sdk/nitric/queues"
-)
-
-func main() {
- transactionQueue := nitric.NewQueue("transactions").Allow(queues.QueueDequeue)
-
- messages, _ := transactionQueue.Dequeue(context.TODO(), 10)
-
- for _, message := range messages {
- // process your message's data
- fmt.Println(message.Message())
-
- // acknowledge when the message is complete
- message.Complete(context.TODO())
- }
-
- nitric.Run()
-}
-```
-
-```dart !!
-import 'package:nitric_sdk/nitric.dart';
-
-final batchQueue = Nitric.queue("batch").allow([
- QueuePermission.dequeue,
-]);
-
-final messages = await batchQueue.dequeue(10);
-
-await Future.wait(messages.map((message) async {
- // process your message's data
- print(message.message);
-
- // acknowledge when the message is complete
- await message.complete();
-}));
-```
-
-
-
-## Choosing between queues and topics
-
-It's common to ask when to use a queue or a topic. From a publisher's point of view, both queues and topics are almost identical. The difference is primarily on the receiver/subscriber side. Topics push new messages to their subscribers, immediately spinning up workers to process them, while queues rely on the receiver to ask for new messages to process.
-
-For these reasons, we usually default to Topics. Queues are more suitable for batch workloads or situations where there are occasional surges of requests that can be processed at a later time.
-
-## Cloud Service Mapping
-
-Each cloud provider comes with a set of default services used when deploying resources. You can find the default services for each cloud provider below.
-
-### Topics
-
-- [AWS](/providers/mappings/aws/topics)
-- [Azure](/providers/mappings/azure/topics)
-- [Google Cloud](/providers/mappings/gcp/topics)
-
-### Queues
-
-- [AWS](/providers/mappings/aws/queues)
-- [Azure](/providers/mappings/azure/queues)
-- [Google Cloud](/providers/mappings/gcp/queues)
diff --git a/docs/providers/mappings/aws/apis.mdx b/docs/providers/mappings/aws/apis.mdx
index 2ea6fe7ea..7fe45eb71 100644
--- a/docs/providers/mappings/aws/apis.mdx
+++ b/docs/providers/mappings/aws/apis.mdx
@@ -28,3 +28,32 @@ During deployment the Nitric CLI builds your API's routes, methods and handlers:
- Lambda ARNs are injected into the API definition using the [x-amazon-apigateway-integration object](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-integration.html), creating an [API Gateway Integration](https://docs.aws.amazon.com/apigateway/api-reference/resource/integration/) for each.
- The API definition is deployed as an API Gateway v2 HTTP API, using the `$default` stage name
- IAM policies are created enabling API Gateway to execute the Lambdas
+
+### Custom Domain Prerequisites
+
+To support custom domains with APIs deployed to AWS your domain (or subdomain) will need to be setup as a [hosted zone](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/hosted-zones-working-with.html) in Route 53.
+
+The general steps to setup a hosted zone in Route 53 are as follows:
+
+1. Navigate to Route 53 in the AWS Console
+2. Select 'hosted zones' from the left navigation
+3. Click 'Create hosted zone'
+4. Enter your domain name and choose the 'Public hosted zone' type.
+5. Click 'Create hosted zone'
+6. You will now be provided with a set of NS DNS records to configure in the DNS provider for your domain
+7. Create the required DNS records, then wait for the DNS changes to propagate
+
+Once this is done you will be able to use the hosted zone domain or any direct subdomain with your Nitric APIs.
+
+You can read more about how AWS suggests configuring hosted zones in their documentation on [Making Route 53 the DNS service for a domain that's in use](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/migrate-dns-domain-in-use.html) or [Making Route 53 the DNS service for an inactive domain](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/migrate-dns-domain-inactive.html).
+
+
+ If the hosted zone was nitric.io, nitric.io or api.nitric.io would be
+ supported for APIs, but not public.api.nitric.io since that is a subdomain of
+ a subdomain.
+
+
+
+ DNS propagation of the NS records can take a few seconds to a few hours due to
+ the nature of DNS.
+
diff --git a/docs/providers/pulumi/azure.mdx b/docs/providers/pulumi/azure.mdx
index 18c466f80..e2900f278 100644
--- a/docs/providers/pulumi/azure.mdx
+++ b/docs/providers/pulumi/azure.mdx
@@ -18,7 +18,7 @@ provider: nitric/azure@latest
Nitric runs services (APIs, Schedules and Topic Subscribers) on Azure
Container Apps, which is unavailable in a [small number of
- regions](https://azure.microsoft.com/en-au/explore/global-infrastructure/products-by-region/?products=container-apps®ions=all).
+ regions](https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/?products=container-apps®ions=all).
The Azure provider is supported by the Nitric SDKs and CLI by default. However, credentials for an Azure account will be required when using the [up command](/reference/cli) from the CLI.
diff --git a/docs/providers/pulumi/gcp.mdx b/docs/providers/pulumi/gcp.mdx
index d48844fd5..2fd19ca09 100644
--- a/docs/providers/pulumi/gcp.mdx
+++ b/docs/providers/pulumi/gcp.mdx
@@ -40,7 +40,14 @@ Download & install the [latest CLI release](https://cloud.google.com/sdk/install
```bash
-curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-378.0.0-linux-x86_64.tar.gz
+# Download the latest version
+curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-x86_64.tar.gz
+
+# Extract the archive
+tar -xf google-cloud-cli-linux-x86_64.tar.gz
+
+# Run the install script
+./google-cloud-sdk/install.sh
```
@@ -127,7 +134,7 @@ apis:
# Target an API by its nitric name
my-api:
# provide domains to be used for the api
- description: An Azure API
+ description: A GCP API
# Add CDN configuration, required for websites deployments
# Available since v1.20.0
diff --git a/docs/queues.mdx b/docs/queues.mdx
new file mode 100644
index 000000000..e8554bbd3
--- /dev/null
+++ b/docs/queues.mdx
@@ -0,0 +1,326 @@
+---
+description: 'Using Queues for pull-based messaging with Nitric'
+---
+
+# Queues
+
+Queues provide scalable, decoupled, asynchronous messaging between services. Unlike topics, messages in queues are pulled by consumers rather than pushed, making queues perfect for batch processing and handling workload spikes.
+
+## Quick Start
+
+Here's a minimal example to get you started:
+
+
+
+```javascript !!
+import { queue } from '@nitric/sdk'
+
+const taskQueue = queue('tasks').allow('enqueue', 'dequeue')
+
+// Send a message to the queue
+await taskQueue.enqueue({
+ task: 'process-data',
+ data: {
+ /* ... */
+ },
+})
+
+// Process messages from the queue
+const messages = await taskQueue.dequeue(10)
+for (const message of messages) {
+ console.log(`Processing: ${message.task}`)
+ await message.complete()
+}
+```
+
+```typescript !!
+import { queue } from '@nitric/sdk'
+
+const taskQueue = queue('tasks').allow('enqueue', 'dequeue')
+
+// Send a message to the queue
+await taskQueue.enqueue({
+ task: 'process-data',
+ data: {
+ /* ... */
+ },
+})
+
+// Process messages from the queue
+const messages = await taskQueue.dequeue(10)
+for (const message of messages) {
+ console.log(`Processing: ${message.task}`)
+ await message.complete()
+}
+```
+
+```python !!
+from nitric.resources import queue
+from nitric.application import Nitric
+
+task_queue = queue("tasks").allow("enqueue", "dequeue")
+
+# Send a message to the queue
+await task_queue.enqueue({
+ "task": "process-data",
+ "data": { /* ... */ }
+})
+
+# Process messages from the queue
+messages = await task_queue.dequeue(10)
+for message in messages:
+ print(f"Processing: {message.task}")
+ await message.complete()
+
+Nitric.run()
+```
+
+```go !!
+import (
+ "context"
+ "github.com/nitrictech/go-sdk/nitric"
+ "github.com/nitrictech/go-sdk/nitric/queues"
+)
+
+func main() {
+ taskQueue := nitric.NewQueue("tasks").Allow(queues.QueueEnqueue, queues.QueueDequeue)
+
+ // Send a message to the queue
+ _ = taskQueue.Enqueue(context.TODO(), []map[string]interface{}{
+ {
+ "task": "process-data",
+ "data": map[string]interface{}{ /* ... */ },
+ },
+ })
+
+ // Process messages from the queue
+ messages, _ := taskQueue.Dequeue(context.TODO(), 10)
+ for _, message := range messages {
+ task := message.Message()["task"].(string)
+ fmt.Printf("Processing: %s\n", task)
+ message.Complete(context.TODO())
+ }
+
+ nitric.Run()
+}
+```
+
+```dart !!
+import 'package:nitric_sdk/nitric.dart';
+
+final taskQueue = Nitric.queue("tasks").allow([
+ QueuePermission.enqueue,
+ QueuePermission.dequeue,
+]);
+
+// Send a message to the queue
+await taskQueue.enqueue([{
+ "task": "process-data",
+ "data": { /* ... */ }
+}]);
+
+// Process messages from the queue
+final messages = await taskQueue.dequeue(10);
+await Future.wait(messages.map((message) async {
+ print("Processing: ${message.task}");
+ await message.complete();
+}));
+```
+
+
+
+## Core Concepts
+
+### Queues
+
+A queue is a named resource where messages can be sent and retrieved. Unlike topics, messages in queues are pulled by consumers rather than pushed, making queues ideal for batch processing and handling workload spikes.
+
+### Message Leasing
+
+When messages are dequeued, they are temporarily hidden from other consumers through a leasing mechanism. This prevents multiple consumers from processing the same message simultaneously.
+
+### Permissions
+
+Queues require explicit permissions for operations:
+
+- `enqueue`: Send messages to the queue
+- `dequeue`: Retrieve messages from the queue
+
+## Common Operations
+
+### Sending Messages
+
+
+
+```javascript !!
+const queue = queue('my-queue').allow('enqueue')
+
+// Send a single message
+await queue.enqueue({
+ message: 'Hello consumer!',
+})
+
+// Send multiple messages
+await queue.enqueue([{ message: 'Task 1' }, { message: 'Task 2' }])
+```
+
+```typescript !!
+const queue = queue('my-queue').allow('enqueue')
+
+// Send a single message
+await queue.enqueue({
+ message: 'Hello consumer!',
+})
+
+// Send multiple messages
+await queue.enqueue([{ message: 'Task 1' }, { message: 'Task 2' }])
+```
+
+```python !!
+queue = queue("my-queue").allow("enqueue")
+
+# Send a single message
+await queue.enqueue({
+ "message": "Hello consumer!"
+})
+
+# Send multiple messages
+await queue.enqueue([
+ { "message": "Task 1" },
+ { "message": "Task 2" }
+])
+```
+
+```go !!
+queue := nitric.NewQueue("my-queue").Allow(queues.QueueEnqueue)
+
+// Send a single message
+_ = queue.Enqueue(context.TODO(), []map[string]interface{}{
+ {"message": "Hello consumer!"},
+})
+
+// Send multiple messages
+_ = queue.Enqueue(context.TODO(), []map[string]interface{}{
+ {"message": "Task 1"},
+ {"message": "Task 2"},
+})
+```
+
+```dart !!
+final queue = Nitric.queue("my-queue").allow([
+ QueuePermission.enqueue,
+]);
+
+// Send a single message
+await queue.enqueue([{
+ "message": "Hello consumer!"
+}]);
+
+// Send multiple messages
+await queue.enqueue([
+ { "message": "Task 1" },
+ { "message": "Task 2" }
+]);
+```
+
+
+
+### Processing Messages
+
+
+
+```javascript !!
+const queue = queue('my-queue').allow('dequeue')
+
+// Dequeue and process messages
+const messages = await queue.dequeue(10)
+for (const message of messages) {
+ console.log(`Processing: ${message.message}`)
+ await message.complete()
+}
+```
+
+```typescript !!
+const queue = queue('my-queue').allow('dequeue')
+
+// Dequeue and process messages
+const messages = await queue.dequeue(10)
+for (const message of messages) {
+ console.log(`Processing: ${message.message}`)
+ await message.complete()
+}
+```
+
+```python !!
+queue = queue("my-queue").allow("dequeue")
+
+// Dequeue and process messages
+messages = await queue.dequeue(10)
+for message in messages:
+ print(f"Processing: {message.message}")
+ await message.complete()
+```
+
+```go !!
+queue := nitric.NewQueue("my-queue").Allow(queues.QueueDequeue)
+
+// Dequeue and process messages
+messages, _ := queue.Dequeue(context.TODO(), 10)
+for _, message := range messages {
+ msg := message.Message()["message"].(string)
+ fmt.Printf("Processing: %s\n", msg)
+ message.Complete(context.TODO())
+}
+```
+
+```dart !!
+final queue = Nitric.queue("my-queue").allow([
+ QueuePermission.dequeue,
+]);
+
+// Dequeue and process messages
+final messages = await queue.dequeue(10);
+await Future.wait(messages.map((message) async {
+ print("Processing: ${message.message}");
+ await message.complete();
+}));
+```
+
+
+
+## Cloud Provider Support
+
+Each cloud provider comes with a set of default services used when deploying resources. You can find the default services for each cloud provider below.
+
+
+
+
+
+Queues are mapped to SQS in AWS. For more details about the AWS implementation, including configuration options and limitations, see the [AWS Queues documentation](/providers/mappings/aws/queues).
+
+
+
+
+
+Queues are mapped to Service Bus in Azure. For more details about the Azure implementation, including configuration options and limitations, see the [Azure Queues documentation](/providers/mappings/azure/queues).
+
+
+
+
+
+Queues are mapped to Cloud Pub/Sub in Google Cloud. For more details about the GCP implementation, including configuration options and limitations, see the [Google Cloud Queues documentation](/providers/mappings/gcp/queues).
+
+
+
+
+
+
+ When running locally, queues use a local implementation for development and
+ testing.
+
+
+
+ For best practices and patterns when working with queues, including handling
+ message delivery guarantees and idempotency, see our [Async Messaging Patterns
+ Guide](/guides/async-messaging-patterns).
+
diff --git a/docs/reference/custom-containers.mdx b/docs/reference/custom-containers.mdx
index b5c515605..a4711e44f 100644
--- a/docs/reference/custom-containers.mdx
+++ b/docs/reference/custom-containers.mdx
@@ -61,7 +61,7 @@ RUN apt-get update -y && \
RUN pip install --upgrade pip pipenv
# Copy either requirements.txt or Pipfile
-COPY requirements.tx[t] Pipfil[e] Pipfile.loc[k] ./
+COPY requirements.txt Pipfile Pipfile.lock ./
# Guarantee lock file if we have a Pipfile and no Pipfile.lock
RUN (stat Pipfile && pipenv lock) || echo "No Pipfile found"
diff --git a/docs/reference/env.mdx b/docs/reference/env.mdx
index 1f1f54b99..790f04b1e 100644
--- a/docs/reference/env.mdx
+++ b/docs/reference/env.mdx
@@ -10,10 +10,10 @@ Nitric sets a number of environment variables to help you manage your project. T
If you are running your project in the cloud, build, or the run environment, Nitric provides prefixed Environment Variables, which you can find in the table below.
-| Name | Description |
-| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| NITRIC_STACK_ID | The Stack ID of the project. Example: `coolkat-gcp-4c0wg0hg` |
-| NITRIC_ENVIRONMENT | The Environment that the app is running on. The value can be either `cloud` (for deployed in the cloud), `run` (for stacks running in nitric run), or `build` (for stack code running during the deployment gathering phase). |
+| Name | Description |
+| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
+| NITRIC_STACK_ID | The unique identifier for your deployed stack (e.g., `coolkat-gcp-4c0wg0hg`) |
+| NITRIC_ENVIRONMENT | The environment where the app is running: `cloud` (deployed), `run` (local stack), or `build` (during deployment) |
## Application Environment Variables
diff --git a/docs/reference/nodejs/secrets/secret-put.mdx b/docs/reference/nodejs/secrets/secret-put.mdx
index 66701f38a..4cedab1e2 100644
--- a/docs/reference/nodejs/secrets/secret-put.mdx
+++ b/docs/reference/nodejs/secrets/secret-put.mdx
@@ -40,7 +40,12 @@ import { secret } from '@nitric/sdk'
const keyRef = secret('apiKey').allow('put')
-await keyRef.put('6c3199a3-094e-4797-bfc9-9ee2a7839286')
+try {
+ await keyRef.put('6c3199a3-094e-4797-bfc9-9ee2a7839286')
+ console.log('Secret value stored successfully')
+} catch (error) {
+ console.error('Error storing secret value:', error)
+}
```
### Get the id of a new secret version
@@ -52,9 +57,10 @@ import { secret } from '@nitric/sdk'
const keyRef = secret('apiKey').allow('put')
-const newApiKeyVersionRef = await keyRef.put(
- '6c3199a3-094e-4797-bfc9-9ee2a7839286',
-)
-
-const versionId = newApiKeyVersionRef.version
+try {
+ const newVersion = await keyRef.put('6c3199a3-094e-4797-bfc9-9ee2a7839286')
+ console.log('New secret version created:', newVersion.version)
+} catch (error) {
+ console.error('Error creating new secret version:', error)
+}
```
diff --git a/docs/reference/nodejs/sql/sql-connection-string.mdx b/docs/reference/nodejs/sql/sql-connection-string.mdx
index 8fbe97f1f..2b00f36dd 100644
--- a/docs/reference/nodejs/sql/sql-connection-string.mdx
+++ b/docs/reference/nodejs/sql/sql-connection-string.mdx
@@ -30,7 +30,12 @@ import { sql } from '@nitric/sdk'
const db = sql('my-data')
// Should be called at runtime, such as in a service handler
-const connStr = await db.connectionString()
+try {
+ const connStr = await db.connectionString()
+ console.log('Database connection string:', connStr)
+} catch (error) {
+ console.error('Error getting connection string:', error)
+}
```
### Connect with Prisma
diff --git a/docs/reference/nodejs/storage/bucket-file-read.mdx b/docs/reference/nodejs/storage/bucket-file-read.mdx
index 096131cab..af37b5fa3 100644
--- a/docs/reference/nodejs/storage/bucket-file-read.mdx
+++ b/docs/reference/nodejs/storage/bucket-file-read.mdx
@@ -32,5 +32,10 @@ const assets = bucket('assets').allow('read')
const logo = assets.file('images/logo.png')
-const logoData = await logo.read()
+try {
+ const logoData = await logo.read()
+ console.log('Logo data:', logoData)
+} catch (error) {
+ console.error('Error reading logo:', error)
+}
```
diff --git a/docs/reference/nodejs/storage/bucket-file-write.mdx b/docs/reference/nodejs/storage/bucket-file-write.mdx
index 0d307e3be..c23aa8ef2 100644
--- a/docs/reference/nodejs/storage/bucket-file-write.mdx
+++ b/docs/reference/nodejs/storage/bucket-file-write.mdx
@@ -17,8 +17,16 @@ import { bucket } from '@nitric/sdk'
const assets = bucket('assets').allow('write')
const logo = assets.file('images/logo.png')
-
-await logo.write(someImageData)
+const imageData = new Uint8Array([
+ /* image data */
+])
+
+try {
+ await logo.write(imageData)
+ console.log('Logo written successfully')
+} catch (error) {
+ console.error('Error writing logo:', error)
+}
```
## Parameters
@@ -41,5 +49,10 @@ const assets = bucket('assets').allow('write')
const txt = assets.file('my-text-file.txt')
const buffer = Buffer.from('My Test File...')
-await txt.write(buffer)
+try {
+ await txt.write(buffer)
+ console.log('Text file written successfully')
+} catch (error) {
+ console.error('Error writing text file:', error)
+}
```
diff --git a/docs/reference/nodejs/storage/bucket-on.mdx b/docs/reference/nodejs/storage/bucket-on.mdx
index 4870dfab9..0f22b8730 100644
--- a/docs/reference/nodejs/storage/bucket-on.mdx
+++ b/docs/reference/nodejs/storage/bucket-on.mdx
@@ -29,9 +29,12 @@ assets.on('write', '/images/cat', (ctx) => {
// If `on` is called with a permissioned bucket, a file will also be provided with the request
accessibleAssets.on('write', '/images/dog', async (ctx) => {
- const dogImage = await ctx.req.file.read()
-
- console.log(dogImage)
+ try {
+ const dogImage = await ctx.req.file.read()
+ console.log(dogImage)
+ } catch (error) {
+ console.error('Error reading dog image:', error)
+ }
})
```
diff --git a/docs/reference/nodejs/websocket/websocket-on.mdx b/docs/reference/nodejs/websocket/websocket-on.mdx
index f954ae573..9c9a53910 100644
--- a/docs/reference/nodejs/websocket/websocket-on.mdx
+++ b/docs/reference/nodejs/websocket/websocket-on.mdx
@@ -78,8 +78,13 @@ const socket = websocket('socket')
const connections = kv('connections').allow('get', 'set', 'delete')
socket.on('connect', async (ctx) => {
- const connectionList = await connections.get('connections')
- await connections..set('connections', [
+ let connectionList = []
+ try {
+ connectionList = await connections.get('connections')
+ } catch (e) {
+ console.log('Creating new connections store')
+ }
+ await connections.set('connections', [
...connectionList,
{
// store any metadata related to the connection here
@@ -89,10 +94,16 @@ socket.on('connect', async (ctx) => {
})
socket.on('disconnect', async (ctx) => {
- const connectionList = await connections.get('connections')
+ let connectionList = []
+ try {
+ connectionList = await connections.get('connections')
+ } catch (e) {
+ console.log('No connections found')
+ return
+ }
await connections.set(
'connections',
- connectionList.filter((c) => c.connectionId === ctx.req.connectionId)
+ connectionList.filter((c) => c.connectionId === ctx.req.connectionId),
)
})
```
diff --git a/docs/reference/nodejs/websocket/websocket-send.mdx b/docs/reference/nodejs/websocket/websocket-send.mdx
index aafdd8def..db857d131 100644
--- a/docs/reference/nodejs/websocket/websocket-send.mdx
+++ b/docs/reference/nodejs/websocket/websocket-send.mdx
@@ -77,7 +77,13 @@ socket.on('disconnect', async (ctx) => {
})
const broadcast = async (data) => {
- const conns = await connections.get('connections')
+ let conns = {}
+ try {
+ conns = await connections.get('connections')
+ } catch (e) {
+ console.log('No connections found')
+ return
+ }
await Promise.all(
Object.keys(conns).map(async (connectionId) => {
diff --git a/docs/schedules.mdx b/docs/schedules.mdx
index 5db1d9223..d36d23ab4 100644
--- a/docs/schedules.mdx
+++ b/docs/schedules.mdx
@@ -50,7 +50,7 @@ schedule('send-reminder').cron('0 22 * * 1-5', async (ctx) => {
```python !!
from nitric.resources import schedule
-from nitric.resources import Nitric
+from nitric.application import Nitric
# Run every 5 minutes
processor = schedule("process-transactions")
@@ -79,7 +79,7 @@ import (
func main() {
// Run every 5 minutes
- nitric.NewSchedule("process-transactions").Every("5 minutues", func(ctx *schedules.Ctx) {
+ nitric.NewSchedule("process-transactions").Every("5 minutes", func(ctx *schedules.Ctx) {
fmt.Printf("processing at %s\n", time.Now().Format(time.RFC3339))
})
// Run at 22:00 Monday through Friday.
@@ -97,15 +97,13 @@ import 'package:nitric_sdk/nitric.dart';
// run every 5 minutes
Nitric.schedule("process-transactions").every("5 minutes", (ctx) async {
// add code to run here
- print("processing transaction")
- return ctx;
+ print("processing transaction");
});
// Run at 22:00 Monday through Friday
Nitric.schedule("send-reminder").cron("0 22 * * 1-5", (ctx) async {
// add code to run here
print("sending reminder");
- return ctx;
});
```
diff --git a/docs/secrets.mdx b/docs/secrets.mdx
index 03be49915..b3f84d6ae 100644
--- a/docs/secrets.mdx
+++ b/docs/secrets.mdx
@@ -132,7 +132,7 @@ api_key = secret("api-key").allow("put")
latest = await api_key.put("a new secret value")
# We can get the version ID of our newly stored value
-latest.version
+print(latest.version)
Nitric.run()
```
diff --git a/docs/sql.mdx b/docs/sql.mdx
index bc222200c..f90de8917 100644
--- a/docs/sql.mdx
+++ b/docs/sql.mdx
@@ -9,10 +9,9 @@ Nitric provides functionality for provisioning and interacting with SQL database
If you're interested in the architecture, provisioning, or deployment steps, they can be found [here](/architecture/sql).
- SQL databases are currently in preview and only support PostgreSql deployed to
- AWS using the
- `nitric/aws` and `nitric/awstf` providers, GCP with the `nitric/azure`
- provider, or Azure with the `nitric/azure` provider.
+ SQL databases are currently in preview and only support PostgreSQL deployed to
+ AWS using the `nitric/aws` and `nitric/awstf` providers, GCP using the
+ `nitric/gcp` provider, or Azure using the `nitric/azure` provider.
diff --git a/docs/storage.mdx b/docs/storage.mdx
index a96f5f056..b41f1cf7b 100644
--- a/docs/storage.mdx
+++ b/docs/storage.mdx
@@ -6,58 +6,82 @@ description: 'Working with files and storage in Nitric'
Nitric provides storage support for securely storing and retrieving large files in the cloud.
-If you're interested in the architecture, provisioning, or deployment steps, they can be found [here](/architecture/buckets).
+## Quick Start
-## Definitions
-
-### Files
-
-Files refer to binary files, such as documents (.doc, .pdf), images (.gif, .jpg), or videos (.mp4, .mkv). It is common for these files to be referred to as BLOBs, which stands for Binary Large Objects. Storing these types of files in buckets, as opposed to other data persistence systems like SQL or document databases, can improve performance.
-
-### Buckets
-
-Buckets serve as isolated repositories for files. To illustrate, think of a bucket as a partition on a traditional hard drive system. Creating separate buckets becomes necessary when storing files for distinct purposes or with varying access control requirements. For instance, you might establish an "uploads" bucket to handle user file uploads or a "profiles" bucket specifically designed for storing user profile images.
-
-### Folders
-
-Most cloud object/blob storage services function as key-value systems, where the keys represent filenames and the values contain the actual file data. By incorporating paths within filenames, you can organize files within a bucket, similar to how folders are utilized in local storage. For example, if you have a file named `profile.png`, you can store it in the folder `images/default` by specifying the complete file path when reading or writing the file, e.g., `images/default/profile.png`.
-
-## Create Buckets
-
-Nitric enables you to define named buckets with customizable permissions. When defining a bucket, you can specify permissions for reading, writing, and deleting files within the bucket. In the example below, we declare a bucket named `profiles` and indicate that our service requires access to read, write, and delete files within that bucket:
+Here's a minimal example to get you started:
```javascript !!
import { bucket } from '@nitric/sdk'
-const profiles = bucket('profiles').allow('read', 'write', 'delete')
+// Declare resources at the module level
+const profiles = bucket('profiles').allow('read', 'write')
+
+// Use resources inside functions
+async function handleProfileImage() {
+ // Upload a file
+ await profiles.file('profile.png').write('image data')
+
+ // Download a file
+ const image = await profiles.file('profile.png').read()
+}
```
```typescript !!
import { bucket } from '@nitric/sdk'
-const profiles = bucket('profiles').allow('read', 'write', 'delete')
+// Declare resources at the module level
+const profiles = bucket('profiles').allow('read', 'write')
+
+// Use resources inside functions
+async function handleProfileImage() {
+ // Upload a file
+ await profiles.file('profile.png').write('image data')
+
+ // Download a file
+ const image = await profiles.file('profile.png').read()
+}
```
```python !!
from nitric.resources import bucket
from nitric.application import Nitric
-profiles = bucket('profiles').allow('read', 'write', 'delete')
+// Declare resources at the module level
+profiles = bucket('profiles').allow('read', 'write')
+
+// Use resources inside functions
+async def handle_profile_image():
+ # Upload a file
+ await profiles.file('profile.png').write(b'image data')
+
+ # Download a file
+ image = await profiles.file('profile.png').read()
Nitric.run()
```
```go !!
import (
+ "context"
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/storage"
)
-func main() {
- profiles := nitric.NewBucket("profiles").Allow(storage.BucketRead, storage.BucketWrite, storage.BucketDelete)
+// Declare resources at the module level
+var profiles = nitric.NewBucket("profiles").Allow(storage.BucketRead, storage.BucketWrite)
+
+// Use resources inside functions
+func handleProfileImage(ctx context.Context) {
+ // Upload a file
+ _ = profiles.Write(ctx, "profile.png", []byte("image data"))
+ // Download a file
+ image, _ := profiles.Read(ctx, "profile.png")
+}
+
+func main() {
nitric.Run()
}
```
@@ -65,132 +89,145 @@ func main() {
```dart !!
import 'package:nitric_sdk/nitric.dart';
+// Declare resources at the module level
final profiles = Nitric.bucket("profiles").allow([
BucketPermission.read,
BucketPermission.write,
- BucketPermission.delete,
]);
+
+// Use resources inside functions
+Future handleProfileImage() async {
+ // Upload a file
+ await profiles.file("profile.png").write("image data");
+
+ // Download a file
+ final image = await profiles.file("profile.png").read();
+}
```
-## File references
+## Core Concepts
-You can obtain a reference to a `File` within a bucket by either using its name or by listing all the files present in the bucket. Once you have a reference to a file, you can perform various operations on it, including retrieving its contents, writing new content to it, or deleting it.
-
-To get a reference to a specific file in a bucket, use the `file()` method on the bucket reference. This returns a `File` reference so other operations can be performed on the file.
+### Buckets
-
- No network calls are made when you get a file reference using `file()`
-
+Buckets are isolated repositories for files. Each bucket in your application needs a unique name and specific permissions for reading, writing, and deleting files.
```javascript !!
import { bucket } from '@nitric/sdk'
-const profiles = bucket('profiles').allow('read')
-
-const profilePicture = profiles.file('users/bruce-wayne/profile.png')
+// Declare resources at the module level
+const profiles = bucket('profiles').allow('read', 'write', 'delete')
```
```typescript !!
import { bucket } from '@nitric/sdk'
-const profiles = bucket('profiles').allow('read')
-
-const profilePicture = profiles.file('users/bruce-wayne/profile.png')
+// Declare resources at the module level
+const profiles = bucket('profiles').allow('read', 'write', 'delete')
```
```python !!
from nitric.resources import bucket
from nitric.application import Nitric
-profiles = bucket("profiles").allow("read")
-
-profile_picture = profiles.file("users/bruce-wayne/profile.png")
+// Declare resources at the module level
+profiles = bucket('profiles').allow('read', 'write', 'delete')
Nitric.run()
```
```go !!
-// Go does not have File references use methods on the Bucket reference directly
+import (
+ "github.com/nitrictech/go-sdk/nitric"
+ "github.com/nitrictech/go-sdk/nitric/storage"
+)
+
+// Declare resources at the module level
+var profiles = nitric.NewBucket("profiles").Allow(storage.BucketRead, storage.BucketWrite, storage.BucketDelete)
+
+func main() {
+ nitric.Run()
+}
```
```dart !!
import 'package:nitric_sdk/nitric.dart';
+// Declare resources at the module level
final profiles = Nitric.bucket("profiles").allow([
BucketPermission.read,
+ BucketPermission.write,
+ BucketPermission.delete,
]);
-
-final profilePicture = profiles.file("users/bruce-wayne/profile.png");
```
-To list all files in a bucket, use the `files()` method on the bucket reference. The function must have `read` permissions to list the files within a bucket.
+### File References
-A network call is made when you list files using `files()`
+You can get a reference to a file in a bucket using the `file()` method. This returns a `File` reference that you can use to perform operations on the file.
+
+
+ No network calls are made when you get a file reference using `file()`
+
```javascript !!
import { bucket } from '@nitric/sdk'
+// Declare resources at the module level
const profiles = bucket('profiles').allow('read')
-const files = await profiles.files()
-
-files.forEach((file) => {
- console.log(file.name)
-})
+// Use resources inside functions
+async function getProfilePicture() {
+ const profilePicture = profiles.file('users/bruce-wayne/profile.png')
+ return profilePicture
+}
```
```typescript !!
import { bucket } from '@nitric/sdk'
+// Declare resources at the module level
const profiles = bucket('profiles').allow('read')
-const files = await profiles.files()
-
-files.forEach((file) => {
- console.log(file.name)
-})
+// Use resources inside functions
+async function getProfilePicture() {
+ const profilePicture = profiles.file('users/bruce-wayne/profile.png')
+ return profilePicture
+}
```
```python !!
from nitric.resources import bucket
from nitric.application import Nitric
-profiles = bucket("profiles").allow("read")
-
-files = await profiles.files()
+// Declare resources at the module level
+profiles = bucket('profiles').allow('read')
-for file in files:
- print(file.name)
+// Use resources inside functions
+async def get_profile_picture():
+ profile_picture = profiles.file('users/bruce-wayne/profile.png')
+ return profile_picture
Nitric.run()
```
```go !!
import (
- "context"
- "fmt"
-
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/storage"
)
-func main() {
- profiles := nitric.NewBucket("profiles").Allow(storage.BucketRead, storage.BucketWrite, storage.BucketDelete)
-
- files, _ := profiles.ListFiles(context.TODO())
-
- for _, file := range files {
- fmt.Println(file)
- }
+// Declare resources at the module level
+var profiles = nitric.NewBucket("profiles").Allow(storage.BucketRead)
+// Go does not have File references use methods on the Bucket reference directly
+func main() {
nitric.Run()
}
```
@@ -198,26 +235,29 @@ func main() {
```dart !!
import 'package:nitric_sdk/nitric.dart';
+// Declare resources at the module level
final profiles = Nitric.bucket("profiles").allow([
BucketPermission.read,
]);
-final files = await profiles.files();
-
-await Future.wait(files.map((file) async {
- print(file.key);
-}));
+// Use resources inside functions
+Future getProfilePicture() async {
+ final profilePicture = profiles.file("users/bruce-wayne/profile.png");
+ return profilePicture;
+}
```
-## Read files
+## Features
+
+### Reading Files
-You can read the contents of a file directly from your application code using the `read()` method on a file reference. The contents of the file are returned as a byte array.
+Read file contents using the `read()` method:
- If a file with that name does not exist when `read()` is called a `NOT_FOUND`
- error will be thrown.
+ If a file doesn't exist when `read()` is called, a `NOT_FOUND` error will be
+ thrown.
@@ -225,26 +265,40 @@ You can read the contents of a file directly from your application code using th
```javascript !!
import { bucket } from '@nitric/sdk'
+// Declare resources at the module level
const profiles = bucket('profiles').allow('read')
-const image = await profiles.file('users/bruce-wayne/profile.png').read()
+// Use resources inside functions
+async function readProfileImage() {
+ const image = await profiles.file('profile.png').read()
+ return image
+}
```
```typescript !!
import { bucket } from '@nitric/sdk'
+// Declare resources at the module level
const profiles = bucket('profiles').allow('read')
-const image = await profiles.file('users/bruce-wayne/profile.png').read()
+// Use resources inside functions
+async function readProfileImage() {
+ const image = await profiles.file('profile.png').read()
+ return image
+}
```
```python !!
from nitric.resources import bucket
from nitric.application import Nitric
-profiles = bucket("profiles").allow("read")
+// Declare resources at the module level
+profiles = bucket('profiles').allow('read')
-image = await profile.file("users/bruce-wayne/profile.png").read()
+// Use resources inside functions
+async def read_profile_image():
+ image = await profiles.file('profile.png').read()
+ return image
Nitric.run()
```
@@ -252,19 +306,19 @@ Nitric.run()
```go !!
import (
"context"
-
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/storage"
)
-func main() {
- profiles := nitric.NewBucket("profiles").Allow(storage.BucketRead, storage.BucketWrite, storage.BucketDelete)
+// Declare resources at the module level
+var profiles = nitric.NewBucket("profiles").Allow(storage.BucketRead)
- image, err := profiles.Read(context.TODO(), "users/bruce-wayne/profile.png")
- if err != nil {
- // Handle error
- }
+// Use resources inside functions
+func readProfileImage(ctx context.Context) ([]byte, error) {
+ return profiles.Read(ctx, "profile.png")
+}
+func main() {
nitric.Run()
}
```
@@ -272,22 +326,27 @@ func main() {
```dart !!
import 'package:nitric_sdk/nitric.dart';
+// Declare resources at the module level
final profiles = Nitric.bucket("profiles").allow([
BucketPermission.read,
]);
-final image = await profiles.file("users/bruce-wayne/profile.png").read();
+// Use resources inside functions
+Future> readProfileImage() async {
+ final image = await profiles.file("profile.png").read();
+ return image;
+}
```
-## Write files
+### Writing Files
-You can write the contents of a file directly from your application code using the `write()` method on a file reference. If the file doesn't exist then it will create a new one, if it does exist then it will overwrite the previous data.
+Write file contents using the `write()` method. If the file doesn't exist, it will be created. If it does exist, it will be overwritten.
- Written data cannot exceed 4MB. If you need to upload more than that, consider
- using an [upload url](#signed-urls).
+ Written data cannot exceed 4MB. For larger files, use [signed
+ URLs](#signed-urls).
@@ -295,32 +354,37 @@ You can write the contents of a file directly from your application code using t
```javascript !!
import { bucket } from '@nitric/sdk'
+// Declare resources at the module level
const profiles = bucket('profiles').allow('write')
-const profileImage = 'image data'
-
-await profiles.file('users/bruce-wayne/profile.png').write(profileImage)
+// Use resources inside functions
+async function writeProfileImage() {
+ await profiles.file('profile.png').write('image data')
+}
```
```typescript !!
import { bucket } from '@nitric/sdk'
+// Declare resources at the module level
const profiles = bucket('profiles').allow('write')
-const profileImage = 'image data'
-
-await profiles.file('users/bruce-wayne/profile.png').write(profileImage)
+// Use resources inside functions
+async function writeProfileImage() {
+ await profiles.file('profile.png').write('image data')
+}
```
```python !!
from nitric.resources import bucket
from nitric.application import Nitric
-profiles = bucket("profiles").allow("write")
-
-profileImage = b"image data"
+// Declare resources at the module level
+profiles = bucket('profiles').allow('write')
-await profiles.file("users/bruce-wayne/profile.png").write(profileImage)
+// Use resources inside functions
+async def write_profile_image():
+ await profiles.file('profile.png').write(b'image data')
Nitric.run()
```
@@ -328,18 +392,19 @@ Nitric.run()
```go !!
import (
"context"
-
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/storage"
)
-func main() {
- profiles := nitric.NewBucket("profiles").Allow(storage.BucketWrite)
-
- profileImage := []byte("image data")
+// Declare resources at the module level
+var profiles = nitric.NewBucket("profiles").Allow(storage.BucketWrite)
- _ = profiles.Write(context.TODO(), "users/bruce-wayne/profile.png", profileImage)
+// Use resources inside functions
+func writeProfileImage(ctx context.Context) error {
+ return profiles.Write(ctx, "profile.png", []byte("image data"))
+}
+func main() {
nitric.Run()
}
```
@@ -347,46 +412,59 @@ func main() {
```dart !!
import 'package:nitric_sdk/nitric.dart';
+// Declare resources at the module level
final profiles = Nitric.bucket("profiles").allow([
BucketPermission.write,
]);
-final profileImage = "image data";
-
-await profiles.file("users/bruce-wayne/profile.png").write(profileImage);
+// Use resources inside functions
+Future writeProfileImage() async {
+ await profiles.file("profile.png").write("image data");
+}
```
-## Delete files
+### Deleting Files
-You can delete a file directly from your service code using the `delete()` method on a file reference.
+Delete files using the `delete()` method:
```javascript !!
import { bucket } from '@nitric/sdk'
+// Declare resources at the module level
const profiles = bucket('profiles').allow('delete')
-await profiles.file('users/bruce-wayne/profile.png').delete()
+// Use resources inside functions
+async function deleteProfileImage() {
+ await profiles.file('profile.png').delete()
+}
```
```typescript !!
import { bucket } from '@nitric/sdk'
+// Declare resources at the module level
const profiles = bucket('profiles').allow('delete')
-await profiles.file('users/bruce-wayne/profile.png').delete()
+// Use resources inside functions
+async function deleteProfileImage() {
+ await profiles.file('profile.png').delete()
+}
```
```python !!
from nitric.resources import bucket
from nitric.application import Nitric
-profiles = bucket("profiles").allow("delete")
+// Declare resources at the module level
+profiles = bucket('profiles').allow('delete')
-await profile.file("users/bruce-wayne/profile.png").delete()
+// Use resources inside functions
+async def delete_profile_image():
+ await profiles.file('profile.png').delete()
Nitric.run()
```
@@ -394,16 +472,19 @@ Nitric.run()
```go !!
import (
"context"
-
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/storage"
)
-func main() {
- profiles := nitric.NewBucket("profiles").Allow(storage.BucketDelete)
+// Declare resources at the module level
+var profiles = nitric.NewBucket("profiles").Allow(storage.BucketDelete)
- _ = profiles.Delete(context.TODO(), "users/bruce-wayne/profile.png")
+// Use resources inside functions
+func deleteProfileImage(ctx context.Context) error {
+ return profiles.Delete(ctx, "profile.png")
+}
+func main() {
nitric.Run()
}
```
@@ -411,68 +492,69 @@ func main() {
```dart !!
import 'package:nitric_sdk/nitric.dart';
+// Declare resources at the module level
final profiles = Nitric.bucket("profiles").allow([
BucketPermission.delete,
]);
-await profiles.file("users/bruce-wayne/profile.png").delete();
+// Use resources inside functions
+Future deleteProfileImage() async {
+ await profiles.file("profile.png").delete();
+}
```
-## Signed URLs
+### Listing Files
-The signed URL feature enables you to generate temporary URLs for downloading or uploading files. These URLs include authentication information in the query string, allowing users without credentials to access the file. They are particularly useful when you need to share temporary links for file downloads or uploads.
+List all files in a bucket using the `files()` method:
-You have the option to customize the expiration time of the URL by specifying a duration in seconds, ranging from 0 to 604,800 (a week). To obtain a download URL, the service needs to have `read` permissions on the bucket. For an upload URL, the service needs to have `write` permissions on the bucket.
-
-
- It's important to note that anyone with the signed URL can access or modify
- the file. Hence, it's crucial to exercise caution and only share the URL with
- trusted users.
-
+A network call is made when you list files using `files()`
```javascript !!
import { bucket } from '@nitric/sdk'
-const profiles = bucket('profiles').allow('read', 'write')
-
-// Get pre-signed URLs for downloading or uploading
-const downloadUrl = await profiles.file('profile.png').getDownloadUrl({
- expiry: 3600, // Expiry defaults to 600 (10 minutes)
-})
+// Declare resources at the module level
+const profiles = bucket('profiles').allow('read')
-const uploadUrl = await profiles.file('profile.png').getUploadUrl({
- expiry: 3600, // Expiry defaults to 600 (10 minutes)
-})
+// Use resources inside functions
+async function listProfileImages() {
+ const files = await profiles.files()
+ files.forEach((file) => {
+ console.log(file.name)
+ })
+}
```
```typescript !!
import { bucket } from '@nitric/sdk'
-const profiles = bucket('profiles').allow('read', 'write')
-
-// Get pre-signed URLs for downloading or uploading
-const downloadUrl = await profiles.file('profile.png').getDownloadUrl({
- expiry: 3600, // Expiry defaults to 600 (10 minutes)
-})
+// Declare resources at the module level
+const profiles = bucket('profiles').allow('read')
-const uploadUrl = await profiles.file('profile.png').getUploadUrl({
- expiry: 3600, // Expiry defaults to 600 (10 minutes)
-})
+// Use resources inside functions
+async function listProfileImages() {
+ const files = await profiles.files()
+ files.forEach((file) => {
+ console.log(file.name)
+ })
+}
```
```python !!
from nitric.resources import bucket
from nitric.application import Nitric
-profiles = bucket('profiles').allow('read', 'write')
+// Declare resources at the module level
+profiles = bucket('profiles').allow('read')
-download_url = await profiles.file('profile.png').download_url(3600) # Expiry defaults to 600 (10 minutes)
-
-upload_url = await profiles.file('profile.png').upload_url(3600) # Expiry defaults to 600 (10 minutes)
+// Use resources inside functions
+async def list_profile_images():
+ files = await profiles.files()
+ for file in files:
+ print(file.name)
Nitric.run()
```
@@ -480,21 +562,27 @@ Nitric.run()
```go !!
import (
"context"
- "time"
-
+ "fmt"
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/storage"
)
-func main() {
- profiles := nitric.NewBucket("profiles").Allow(storage.BucketRead, storage.BucketWrite)
-
- // Expiry defaults to 600 (10 minutes)
- downloadUrl, _ := profiles.DownloadUrl(context.TODO(), "profile.png", storage.WithPresignUrlExpiry(time.Millisecond*3600))
+// Declare resources at the module level
+var profiles = nitric.NewBucket("profiles").Allow(storage.BucketRead)
- // Expiry defaults to 600 (10 minutes)
- uploadUrl, _ := profiles.UploadUrl(context.TODO(), "profile.png", storage.WithPresignUrlExpiry(time.Millisecond*3600))
+// Use resources inside functions
+func listProfileImages(ctx context.Context) error {
+ files, err := profiles.ListFiles(ctx)
+ if err != nil {
+ return err
+ }
+ for _, file := range files {
+ fmt.Println(file)
+ }
+ return nil
+}
+func main() {
nitric.Run()
}
```
@@ -502,32 +590,29 @@ func main() {
```dart !!
import 'package:nitric_sdk/nitric.dart';
+// Declare resources at the module level
final profiles = Nitric.bucket("profiles").allow([
BucketPermission.read,
- BucketPermission.write
]);
-final downloadUrl = await profiles.file("profile.png").getDownloadUrl(
- expiry: 3600 // Expiry defaults to 600 (10 minutes)
-);
-
-final uploadUrl = await profiles.file("profile.png").getUploadUrl(
- expiry: 3600 // Expiry defaults to 600 (10 minutes)
-);
+// Use resources inside functions
+Future listProfileImages() async {
+ final files = await profiles.files();
+ await Future.wait(files.map((file) async {
+ print(file.key);
+ }));
+}
```
-## Bucket Notifications
-
-Bucket notifications are services that subscribe to changes in your bucket's files. You can configure these notifications to trigger specifically when a file is written or deleted. Additionally, you can apply filters based on file prefixes to specify which files should trigger a notification. When the service is triggered, it receives event data that includes details about the changed file, such as whether it was written or deleted, and the file's name.
+### Signed URLs
-To illustrate, suppose we want to trigger a service whenever a new profile image is uploaded to a bucket. In this case, we can use the following code snippet:
+Generate temporary URLs for downloading or uploading files. These URLs include authentication information and are useful for sharing temporary links.
- Writing/Delete to a bucket from within a bucket notification will trigger the
- same notification to run again (if it meets the requirement). This will cause
- an infinite loop, which is highly undesirable for your cloud bill.
+ Anyone with the signed URL can access or modify the file. Only share URLs with
+ trusted users.
@@ -535,55 +620,84 @@ To illustrate, suppose we want to trigger a service whenever a new profile image
```javascript !!
import { bucket } from '@nitric/sdk'
-const profiles = bucket('profiles')
+// Declare resources at the module level
+const profiles = bucket('profiles').allow('read', 'write')
-// Filter for 'write' events for files starting with '/users/images'
-profiles.on('write', '/users/images', (ctx) => {
- console.log(`new profile image for ${ctx.req.key} was created`)
-})
+// Use resources inside functions
+async function getSignedUrls() {
+ const downloadUrl = await profiles.file('profile.png').getDownloadUrl({
+ expiry: 3600, // Expires in 1 hour
+ })
+
+ const uploadUrl = await profiles.file('profile.png').getUploadUrl({
+ expiry: 3600, // Expires in 1 hour
+ })
+
+ return { downloadUrl, uploadUrl }
+}
```
```typescript !!
import { bucket } from '@nitric/sdk'
-const profiles = bucket('profiles')
+// Declare resources at the module level
+const profiles = bucket('profiles').allow('read', 'write')
-// Filter for 'write' events for files starting with '/users/images'
-profiles.on('write', '/users/images', (ctx) => {
- console.log(`new profile image for ${ctx.req.key} was created`)
-})
+// Use resources inside functions
+async function getSignedUrls() {
+ const downloadUrl = await profiles.file('profile.png').getDownloadUrl({
+ expiry: 3600, // Expires in 1 hour
+ })
+
+ const uploadUrl = await profiles.file('profile.png').getUploadUrl({
+ expiry: 3600, // Expires in 1 hour
+ })
+
+ return { downloadUrl, uploadUrl }
+}
```
```python !!
from nitric.resources import bucket
from nitric.application import Nitric
-profiles = bucket("profiles")
+// Declare resources at the module level
+profiles = bucket('profiles').allow('read', 'write')
-# Filter for 'write' events for files starting with '/users/images'
-@profiles.on("write", "/users/images")
-async def image_written(ctx):
- print(f"new profile image for {ctx.req.key} was written")
+// Use resources inside functions
+async def get_signed_urls():
+ download_url = await profiles.file('profile.png').download_url(3600) # Expires in 1 hour
+ upload_url = await profiles.file('profile.png').upload_url(3600) # Expires in 1 hour
+ return {'download_url': download_url, 'upload_url': upload_url}
Nitric.run()
```
```go !!
import (
- "fmt"
-
+ "context"
+ "time"
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/storage"
)
-func main() {
- profiles := nitric.NewBucket("profiles")
+// Declare resources at the module level
+var profiles = nitric.NewBucket("profiles").Allow(storage.BucketRead, storage.BucketWrite)
- // Filter for 'write' events for files starting with '/users/images'
- profiles.On(storage.WriteNotification, "/users/images", func(ctx *storage.Ctx) {
- fmt.Printf("new profile image for %s was written", ctx.Request.Key())
- })
+// Use resources inside functions
+func getSignedUrls(ctx context.Context) (string, string, error) {
+ downloadUrl, err := profiles.DownloadUrl(ctx, "profile.png", storage.WithPresignUrlExpiry(time.Hour))
+ if err != nil {
+ return "", "", err
+ }
+ uploadUrl, err := profiles.UploadUrl(ctx, "profile.png", storage.WithPresignUrlExpiry(time.Hour))
+ if err != nil {
+ return "", "", err
+ }
+ return downloadUrl, uploadUrl, nil
+}
+func main() {
nitric.Run()
}
```
@@ -591,41 +705,63 @@ func main() {
```dart !!
import 'package:nitric_sdk/nitric.dart';
-final profiles = Nitric.bucket("profiles");
+// Declare resources at the module level
+final profiles = Nitric.bucket("profiles").allow([
+ BucketPermission.read,
+ BucketPermission.write,
+]);
+
+// Use resources inside functions
+Future
-If we instead wanted to trigger the function whenever any file was deleted from the bucket, we would use the following snippet:
+### Bucket Notifications
+
+Subscribe to changes in your bucket's files. You can filter notifications by file prefix and event type (write/delete).
+
+
+ Writing/Deleting to a bucket from within a bucket notification will trigger
+ the same notification again, potentially causing an infinite loop.
+
```javascript !!
import { bucket } from '@nitric/sdk'
+// Declare resources at the module level
const profiles = bucket('profiles')
-// Filter for 'delete' events for any file
-profiles.on('delete', '*', (ctx) => {
- console.log(`${ctx.req.key} was deleted`)
+// Use resources inside functions
+profiles.on('write', '/users/images', (ctx) => {
+ console.log(`New profile image for ${ctx.req.key} was created`)
})
```
```typescript !!
import { bucket } from '@nitric/sdk'
+// Declare resources at the module level
const profiles = bucket('profiles')
-// Filter for 'delete' events for any file
-profiles.on('delete', '*', (ctx) => {
- console.log(`${ctx.req.key} was deleted`)
+// Use resources inside functions
+profiles.on('write', '/users/images', (ctx) => {
+ console.log(`New profile image for ${ctx.req.key} was created`)
})
```
@@ -633,12 +769,13 @@ profiles.on('delete', '*', (ctx) => {
from nitric.resources import bucket
from nitric.application import Nitric
-profiles = bucket("profiles")
+// Declare resources at the module level
+profiles = bucket('profiles')
-# Filter for 'delete' events for any file
-@profiles.on("delete", "*")
-async def file_deleted(ctx):
- print(f"{ctx.req.key} was deleted")
+// Use resources inside functions
+@profiles.on("write", "/users/images")
+async def image_written(ctx):
+ print(f"New profile image for {ctx.req.key} was written")
Nitric.run()
```
@@ -646,19 +783,21 @@ Nitric.run()
```go !!
import (
"fmt"
-
"github.com/nitrictech/go-sdk/nitric"
"github.com/nitrictech/go-sdk/nitric/storage"
)
-func main() {
- profiles := nitric.NewBucket("profiles")
+// Declare resources at the module level
+var profiles = nitric.NewBucket("profiles")
- // Filter for 'delete' events for any file
- profiles.On(storage.DeleteNotification, "*", func(ctx *storage.Ctx) {
- fmt.Printf("%s was deleted", ctx.Request.Key())
+// Use resources inside functions
+func init() {
+ profiles.On(storage.WriteNotification, "/users/images", func(ctx *storage.Ctx) {
+ fmt.Printf("New profile image for %s was written", ctx.Request.Key())
})
+}
+func main() {
nitric.Run()
}
```
@@ -666,14 +805,16 @@ func main() {
```dart !!
import 'package:nitric_sdk/nitric.dart';
+// Declare resources at the module level
final profiles = Nitric.bucket("profiles");
-// Filter for 'delete' events for any file
-profiles.on(BlobEventType.delete, "*", (ctx) async {
- print("${ctx.req.key} was deleted");
-
- return ctx;
-});
+// Use resources inside functions
+void setupNotifications() {
+ profiles.on(BlobEventType.write, "/users/images", (ctx) async {
+ print("New profile image for ${ctx.req.key} was written");
+ return ctx;
+ });
+}
```
diff --git a/docs/topics.mdx b/docs/topics.mdx
new file mode 100644
index 000000000..11e3abab1
--- /dev/null
+++ b/docs/topics.mdx
@@ -0,0 +1,269 @@
+---
+description: 'Using Topics for publish/subscribe messaging with Nitric'
+---
+
+# Topics
+
+Topics provide scalable, decoupled, asynchronous messaging between services. When a message is published to a topic, it's immediately pushed to all subscribers, making topics perfect for real-time event processing.
+
+## Quick Start
+
+Here's a minimal example to get you started:
+
+
+
+```javascript !!
+import { topic } from '@nitric/sdk'
+
+const userCreatedTopic = topic('user-created').allow('publish')
+
+// Publish a message
+await userCreatedTopic.publish({
+ email: 'new.user@example.com',
+})
+
+// Subscribe to messages
+userCreatedTopic.subscribe(async (ctx) => {
+ const { email } = ctx.req.json()
+ console.log(`New user created: ${email}`)
+})
+```
+
+```typescript !!
+import { topic } from '@nitric/sdk'
+
+const userCreatedTopic = topic('user-created').allow('publish')
+
+// Publish a message
+await userCreatedTopic.publish({
+ email: 'new.user@example.com',
+})
+
+// Subscribe to messages
+userCreatedTopic.subscribe(async (ctx) => {
+ const { email } = ctx.req.json()
+ console.log(`New user created: ${email}`)
+})
+```
+
+```python !!
+from nitric.resources import topic
+from nitric.application import Nitric
+
+user_created_topic = topic("user-created").allow("publish")
+
+# Publish a message
+await user_created_topic.publish({
+ "email": "new.user@example.com"
+})
+
+# Subscribe to messages
+@user_created_topic.subscribe()
+async def handle_new_user(ctx):
+ email = ctx.req.data['email']
+ print(f"New user created: {email}")
+
+Nitric.run()
+```
+
+```go !!
+import (
+ "github.com/nitrictech/go-sdk/nitric"
+ "github.com/nitrictech/go-sdk/nitric/topics"
+)
+
+func main() {
+ userCreatedTopic := nitric.NewTopic("user-created").Allow(topics.TopicPublish)
+
+ // Publish a message
+ _ = userCreatedTopic.Publish(context.TODO(), map[string]interface{}{
+ "email": "new.user@example.com",
+ })
+
+ // Subscribe to messages
+ userCreatedTopic.Subscribe(func(ctx *topics.Ctx) {
+ email := ctx.Request.Message()["email"].(string)
+ fmt.Printf("New user created: %s\n", email)
+ })
+
+ nitric.Run()
+}
+```
+
+```dart !!
+import 'package:nitric_sdk/nitric.dart';
+
+final userCreatedTopic = Nitric.topic("user-created").allow([
+ TopicPermission.publish,
+]);
+
+// Publish a message
+await userCreatedTopic.publish({
+ "email": "new.user@example.com"
+});
+
+// Subscribe to messages
+userCreatedTopic.subscribe((ctx) async {
+ final email = ctx.req.message["email"];
+ print("New user created: $email");
+ return ctx;
+});
+```
+
+
+
+## Core Concepts
+
+### Topics
+
+A topic is a named resource where events can be published. Topics enable real-time communication between services, with messages being pushed to subscribers as soon as they're published.
+
+### Subscriptions
+
+A subscription is a binding between a topic and a service. When a message is published to a topic, all subscribers receive a copy of the message.
+
+### Permissions
+
+Topics require explicit permissions for operations:
+
+- `publish`: Send messages to the topic
+- `subscribe`: Receive messages from the topic
+
+## Common Operations
+
+### Publishing Messages
+
+
+
+```javascript !!
+const topic = topic('my-topic').allow('publish')
+
+await topic.publish({
+ message: 'Hello subscribers!',
+})
+```
+
+```typescript !!
+const topic = topic('my-topic').allow('publish')
+
+await topic.publish({
+ message: 'Hello subscribers!',
+})
+```
+
+```python !!
+topic = topic("my-topic").allow("publish")
+
+await topic.publish({
+ "message": "Hello subscribers!"
+})
+```
+
+```go !!
+topic := nitric.NewTopic("my-topic").Allow(topics.TopicPublish)
+
+_ = topic.Publish(context.TODO(), map[string]interface{}{
+ "message": "Hello subscribers!",
+})
+```
+
+```dart !!
+final topic = Nitric.topic("my-topic").allow([
+ TopicPermission.publish,
+]);
+
+await topic.publish({
+ "message": "Hello subscribers!"
+});
+```
+
+
+
+### Subscribing to Messages
+
+
+
+```javascript !!
+const topic = topic('my-topic')
+
+topic.subscribe(async (ctx) => {
+ const { message } = ctx.req.json()
+ console.log(`Received: ${message}`)
+})
+```
+
+```typescript !!
+const topic = topic('my-topic')
+
+topic.subscribe(async (ctx) => {
+ const { message } = ctx.req.json()
+ console.log(`Received: ${message}`)
+})
+```
+
+```python !!
+topic = topic("my-topic")
+
+@topic.subscribe()
+async def handle_message(ctx):
+ message = ctx.req.data['message']
+ print(f"Received: {message}")
+```
+
+```go !!
+topic := nitric.NewTopic("my-topic")
+
+topic.Subscribe(func(ctx *topics.Ctx) {
+ message := ctx.Request.Message()["message"].(string)
+ fmt.Printf("Received: %s\n", message)
+})
+```
+
+```dart !!
+final topic = Nitric.topic("my-topic");
+
+topic.subscribe((ctx) async {
+ final message = ctx.req.message["message"];
+ print("Received: $message");
+ return ctx;
+});
+```
+
+
+
+## Cloud Provider Support
+
+Each cloud provider comes with a set of default services used when deploying resources. You can find the default services for each cloud provider below.
+
+
+
+
+
+Topics are mapped to SNS in AWS. For more details about the AWS implementation, including configuration options and limitations, see the [AWS Topics documentation](/providers/mappings/aws/topics).
+
+
+
+
+
+Topics are mapped to Event Grid in Azure. For more details about the Azure implementation, including configuration options and limitations, see the [Azure Topics documentation](/providers/mappings/azure/topics).
+
+
+
+
+
+Topics are mapped to Pub/Sub in Google Cloud. For more details about the GCP implementation, including configuration options and limitations, see the [Google Cloud Topics documentation](/providers/mappings/gcp/topics).
+
+
+
+
+
+
+ When running locally, topics use a local implementation for development and
+ testing.
+
+
+
+ For best practices and patterns when working with topics, including handling
+ message delivery guarantees and idempotency, see our [Async Messaging Patterns
+ Guide](/guides/async-messaging-patterns).
+
diff --git a/next.config.mjs b/next.config.mjs
index 3246c1d70..7860affdf 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -98,6 +98,12 @@ const nextConfig = {
permanent: true,
}),
),
+ {
+ source: '/docs/messaging',
+ destination: '/docs/topics',
+ basePath: false,
+ permanent: true,
+ },
{
source: '/docs/comparison/:slug',
destination: '/docs/concepts/comparison/:slug',
diff --git a/src/config/index.ts b/src/config/index.ts
index 07326f2cb..2de90a555 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -12,6 +12,7 @@ import {
BeakerIcon,
DocumentDuplicateIcon,
WindowIcon,
+ QueueListIcon,
} from '@heroicons/react/24/outline'
import { SiPulumi, SiTerraform } from 'react-icons/si'
import { NavEntry } from './types'
@@ -209,9 +210,14 @@ export const navigation: NavEntry[] = [
href: '/sql',
},
{
- title: 'Async Messaging',
+ title: 'Topics',
icon: MegaphoneIcon,
- href: '/messaging',
+ href: '/topics',
+ },
+ {
+ title: 'Queues',
+ icon: QueueListIcon,
+ href: '/queues',
},
{
title: 'Secrets',