Tutorial: build the Next.js blog
examples/nextjs-blog shows Tango inside a Next.js App Router project.
The point of the example is that the Tango application-facing layers stay recognizable even when the host runtime changes. Models, Model.objects, migrations, serializers, model hooks, and resources still follow the same patterns you use in the Express example.
1. Run the example
pnpm --filter @danceroutine/tango-example-nextjs-blog bootstrap
pnpm --filter @danceroutine/tango-example-nextjs-blog devThen open:
http://localhost:3001/http://localhost:3001/api/posts?limit=20&offset=0http://localhost:3001/api/openapi
2. Read the model definition
The model module defines:
PostReadSchemaPostCreateSchemaPostModel
It also imports @danceroutine/tango-orm/runtime, which is what enables PostModel.objects without separate runtime glue elsewhere in the app.
The model is also where the example keeps record lifecycle behavior such as slug generation and timestamp stamping. Those rules live in model hooks so they continue to run outside the API layer too.
3. Read the serializer
The serializer is the clearest place to see the resource contract.
Notice what it owns:
- create validation through
PostCreateSchema - update validation through
PostCreateSchema.partial() - output shaping through
PostReadSchema - request-scoped normalization before the manager call
That is the DRF-shaped piece of the example. Zod still defines the schemas, and the serializer gathers the HTTP-facing workflow around them.
4. Read the resource classes
The viewset handles the collection-style API, while the generic detail view handles retrieve, update, and delete for one post.
Two details are worth noticing:
PostViewSetdelegates create and update behavior to the serializer instead of hand-rolling schema parsing in the route layer.PostViewSetdeclares a custompublishaction throughModelViewSet.defineViewSetActions(...).
The App Router route files are also worth reading closely. NextAdapter owns the route-handler wiring while the viewset and generic view classes stay focused on resource behavior.
The example also includes a dedicated OpenAPI route. One module builds the document from resource instances, and the App Router route publishes it as JSON for external tooling.
5. Read the server-rendered pages
The page components still use PostModel.objects directly where that keeps the code straightforward.
The index page also uses TangoQueryParams.fromRecord(...) from @danceroutine/tango-core. That keeps page-level query parsing aligned with the same query contract resources use internally.
That gives the example a useful contrast:
- pages can query through
Model.objectswhen they just need data - resources use serializers and view classes when they need a public HTTP contract
- model hooks keep persistence rules aligned across both paths
6. Compare this example to the Express example
Both examples use the same Tango concepts:
- models
Model.objects- migrations
- serializers
- model hooks
- resource classes
The outer adapter layer is the part that changes, while the Tango application-facing core remains recognizable across both examples.
7. Make a small change
Try adding another searchable field or a new filter and trace the change through:
- model and schema definition
- model hooks if the field changes record lifecycle behavior
- serializer contract if the API-facing shape changed
- resource config
- page query behavior if the server-rendered UI should expose the new field
- migration scripts if the schema changed
That exercise will show you which parts of the stack stay fixed and which parts belong to the runtime adapter.