This is a Next.js project bootstrapped with c3.
First, run the development server:
npm run dev
Open http://localhost:3000 with your browser to see the result.
- Node.js 18.x or later
- npm 10.x or later
- A Cloudflare account
- Wrangler CLI installed globally (
npm install -g wrangler)
This project requires certain environment variables to be set up before running. Follow these steps to configure your environment:
-
Copy the example environment file:
cp .env.example .env
-
Open the
.envfile and replace the placeholder values with your actual configuration:- Database settings
- API keys
- Server configuration
-
Make sure never to commit the
.envfile to version control.
| Variable | Description | Example Value |
|---|---|---|
| DIRECT_URL | Direct PostgreSQL connection URL | postgresql://user:pass@host:5432/db |
| DATABASE_URL | Pooled PostgreSQL connection URL | postgresql://user:pass@host:5432/db?pooled=true |
| NEXT_PUBLIC_API_URL | Public API endpoint URL | https://api.yourdomain.com |
| NEXT_PUBLIC_MAILCHIMP_AUDIENCE_ID | Mailchimp audience/list ID | abc123def |
| NEXT_PUBLIC_MAILCHIMP_API_KEY | Mailchimp API key | 1234567890abcdef-us1 |
| NEXT_PUBLIC_MAILCHIMP_API_SERVER | Mailchimp API server region | us1 |
| NEXT_PUBLIC_MAILCHIMP_URL | Mailchimp form submission URL | https://your-form.mailchimp.com |
| NEXT_PUBLIC_CONTACT_FORM_URL | Contact form submission endpoint | https://api.yourdomain.com/contact |
| PORT | Application port | 3000 |
| NODE_ENV | Environment name | development |
- Clone the repository:
git clone <repository-url>
cd ab-photos-live- Install dependencies:
npm install- Generate your
.env.localfile from 1Password:
npm run env:getThis will create a .env.local file with all the necessary environment variables retrieved from 1Password.
- Start the development server:
npm run devThis runs Next.js in development mode with hot reloading at http://localhost:3000
- Preview Cloudflare Pages locally:
npm run previewThis builds and runs your application in an environment that matches Cloudflare Pages
- Build for production:
npm run pages:buildThis creates an optimized production build in the .vercel/output directory
- Run tests:
npm run test # Run all tests
npm run test:watch # Run tests in watch mode- Authenticate with Cloudflare:
wrangler login- Run changes locally with wrangler,
npm run dev:wrangler- Deploy to Cloudflare Pages:
npm run deploy- Use Chrome DevTools with the development server (a debugger is enabled at default port)
- Check the
.vercel/outputdirectory for build output - View build logs with
wrangler pages deployment tail
-
Always test your changes locally using both:
npm run devfor rapid developmentnpm run previewto ensure Cloudflare compatibility
-
Check for type errors:
npm run lint- Run tests before deploying:
npm run test- Use the appropriate environment variables for local development vs production
Besides the dev script mentioned above c3 has added a few extra scripts that allow you to integrate the application with the Cloudflare Pages environment, these are:
pages:buildto build the application for Pages using the@cloudflare/next-on-pagesCLIpreviewto locally preview your Pages application using the Wrangler CLIdeployto deploy your Pages application using the Wrangler CLI
Note: while the
devscript is optimal for local development you should preview your Pages application as well (periodically or before deployments) in order to make sure that it can properly work in the Pages environment (for more details see the@cloudflare/next-on-pagesrecommended workflow)
Cloudflare Bindings are what allows you to interact with resources available in the Cloudflare Platform.
You can use bindings during development, when previewing locally your application and of course in the deployed application:
-
To use bindings in dev mode you need to define them in the
next.config.jsfile undersetupDevBindings, this mode uses thenext-dev@cloudflare/next-on-pagessubmodule. For more details see its documentation. -
To use bindings in the preview mode you need to add them to the
pages:previewscript accordingly to thewrangler pages devcommand. For more details see its documentation or the Pages Bindings documentation. -
To use bindings in the deployed application you will need to configure them in the Cloudflare dashboard. For more details see the Pages Bindings documentation.
c3 has added for you an example showing how you can use a KV binding.
In order to enable the example:
- Search for javascript/typescript lines containing the following comment:
and uncomment the commented lines below it.
// KV Example: - Do the same in the
wrangler.tomlfile, where the comment is:# KV Example: - If you're using TypeScript run the
cf-typegenscript to update theenv.d.tsfile:npm run cf-typegen # or yarn cf-typegen # or pnpm cf-typegen # or bun cf-typegen
After doing this you can run the dev or preview script and visit the /api/hello route to see the example in action.
Finally, if you also want to see the example work in the deployed application make sure to add a MY_KV_NAMESPACE binding to your Pages application in its dashboard kv bindings settings section. After having configured it make sure to re-deploy your application.
We use Jest for testing our codebase. Here are our testing conventions and best practices:
- Place test files in
__tests__directories next to the code being tested - Name test files with the
.test.tsor.test.tsxextension - Mirror the source file structure in test files:
src/
└── db/
├── client.ts
└── __tests__/
└── client.test.ts
Structure your tests using describe blocks for logical grouping:
describe('Component/Module Name', () => {
describe('Specific Function/Feature', () => {
it('should behave in a specific way', () => {
// Test code
});
});
});- Create mock functions at the top of your test file:
const mockFunction = jest.fn();
jest.mock('module-name', () => ({
exportedFunction: mockFunction
}));- Use
mockImplementationfor complex mocks:
mockFunction.mockImplementation(() => {
// Mock implementation
});- Use
mockImplementationOncefor one-time behaviors:
mockFunction.mockImplementationOnce(() => {
throw new Error('Test error');
});- Reset the test environment before each test:
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
});- Clean up after tests:
afterEach(() => {
jest.restoreAllMocks();
});- Always use async/await for asynchronous tests:
it('should handle async operations', async () => {
await expect(asyncFunction()).resolves.toBe(expectedValue);
});Test both success and error cases:
it('should handle errors', async () => {
// Arrange
const mockError = new Error('Test error');
mockFunction.mockRejectedValue(mockError);
// Act & Assert
await expect(functionUnderTest())
.rejects.toThrow('Test error');
});# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests for a specific file
npm test path/to/file.test.ts
# Run tests with coverage
npm test -- --coverage- Aim for 80% code coverage at minimum
- Focus on testing:
- Critical business logic
- Error handling
- Edge cases
- API integrations
We follow a structured logging approach to ensure consistency and debuggability across the application. Here are our logging conventions:
- Use source prefixes to identify the log origin:
// In API routes console.log('[API] routeName: message', context); // In server actions console.log('[Action] actionName: message', context);
Each log should follow this pattern:
console.log('[Source] functionName: descriptiveMessage', {
// Context object with relevant data
param1,
param2,
// Add derived data when useful
resultCount: results.length
});Use appropriate logging levels:
console.log()for general flow and successful operationsconsole.warn()for handled issues (e.g., non-OK responses)console.error()for errors and exceptions
For each operation, log the following stages:
// 1. Starting the operation
console.log('[Source] operation: Starting request', { params });
// 2. Important steps
console.log('[Source] operation: Performing step', { stepDetails });
// 3. Successful completion
console.log('[Source] operation: Successfully completed', { results });
// 4. Error handling
console.warn('[Source] operation: Non-OK response', {
status,
statusText,
// Include relevant context
params
});
// 5. Error cases
console.error('[Source] operation: Error occurred', {
error,
// Include all relevant parameters
params
});Always include relevant context as a second parameter:
// Good
console.log('[API] getPhoto: Fetching photo', { photoId, userId });
// Avoid
console.log('[API] getPhoto: Fetching photo', photoId); // Missing context
console.log(`[API] getPhoto: Fetching photo ${photoId}`); // String interpolationWhen logging errors, include full context:
try {
// Operation
} catch (error) {
console.error('[Source] operation: Error occurred', {
error,
// Include all parameters that led to the error
params,
// Include any relevant state
state
});
}For API responses, log both success and failure cases:
if (!response.ok) {
console.warn('[Source] operation: Non-OK response', {
status: response.status,
statusText: response.statusText,
// Include request parameters
params
});
} else {
console.log('[Source] operation: Success response', {
// Include relevant response data
resultCount: data.length,
// Include request parameters
params
});
}These practices ensure:
- Consistent log format across the application
- Easy filtering and searching in log aggregators
- Sufficient context for debugging issues
- Clear operation lifecycle tracking
- Proper error tracking and debugging