Understanding the Problem
Up to this point, we've been developing and testing our Express authentication application locally on our own computer. While this is perfect for development, eventually we need to make our application available to everyone on the internet. This process of making an application available online is called deployment.
Deployment involves several key challenges:
- Finding a hosting service that can run our Node.js application
- Setting up a production database (PostgreSQL instead of SQLite)
- Configuring environment variables for the production environment
- Building and starting the application in a production environment
- Making our application resilient and maintainable in production
In this phase, we'll deploy our application to Render, a cloud platform that offers free hosting for web services and databases. Render makes it relatively easy to deploy Express applications, but there are several important steps and considerations we need to address.
Planning the Solution
- Set up a package.json file at the project root
- Create a Render.com account
- Create a PostgreSQL database instance on Render
- Create a new web service on Render
- Configure build and start commands
- Set up environment variables
- Deploy the application
- Test the deployed application
- Understand ongoing maintenance requirements
Deployment Strategy: Our strategy follows a common pattern for deploying Node.js applications:
- Prepare the codebase with proper configuration and scripts
- Set up the database before the application (dependency order)
- Configure the application environment with necessary variables
- Deploy and test to verify functionality
- Plan for maintenance to ensure long-term reliability
Deployment Architecture: We'll be creating a simple two-tier architecture:
┌───────────────────────┐ ┌───────────────────────┐
│ │ │ │
│ Web Service │ │ PostgreSQL Database │
│ (Express App) │◄─────────►│ (User data) │
│ │ │ │
└───────────────────────┘ └───────────────────────┘
▲
│
│ HTTP Requests
│
▼
┌───────────────────────┐
│ │
│ Clients │
│ (Web Browsers) │
│ │
└───────────────────────┘
This architecture separates the application logic (web service) from data storage (database), allowing each component to be managed, scaled, and secured independently.
Implementing the Solution
1. Set Up a package.json File at the Project Root
When deploying to Render, we need a package.json file at the root of our project (outside of both the backend and frontend folders). This file will define scripts that Render will use to install dependencies and start our application.
// Navigate to the project root (outside of backend and frontend)
cd ..
npm init -y
Now, let's modify the package.json to include the necessary scripts:
// package.json (in the project root)
{
"name": "your-project-name",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"install": "npm --prefix backend install backend", // Install backend dependencies
"dev:backend": "npm install --prefix backend start", // Start backend in dev mode
"sequelize": "npm run --prefix backend sequelize", // Run sequelize commands
"sequelize-cli": "npm run --prefix backend sequelize-cli", // Run sequelize-cli
"start": "npm start --prefix backend", // Start the backend
"build": "npm run --prefix backend build" // Build the backend
},
"keywords": [],
"author": "",
"license": "ISC"
}
These scripts tell Render:
- install: Run npm install in the backend folder
- dev:backend: (optional) Start the backend in development mode
- sequelize and sequelize-cli: Run Sequelize commands in the backend folder
- start: Start the backend server
- build: Run the build script in the backend folder
Commit these changes to your main branch:
git add .
git commit -m "Add root package.json for deployment"
git checkout main
git merge dev
git push origin main
Why a Root package.json? Render looks for a package.json file in the root of your repository to determine:
- How to install dependencies
- How to build the application
- How to start the application
The --prefix Flag: This tells npm to run commands in a specific directory. For example, npm --prefix backend install runs npm install in the backend directory.
Script Chaining: Notice that some scripts call other scripts. For example, npm run --prefix backend sequelize runs the "sequelize" script defined in the backend's package.json.
Git Branch Management: For deployment, we're merging our development branch (dev) into the main branch. This is a common workflow:
- Develop features in feature branches
- Merge feature branches into dev for integration testing
- Merge dev into main for deployment
2. Set Up a Render.com Account
If you don't already have a Render.com account, you'll need to create one:
- Go to Render.com and click "Get Started"
- Sign up with GitHub (recommended) to easily connect your repositories
- Follow the instructions to complete your registration and verify your account
GitHub Integration: Signing up with GitHub offers several advantages:
- Easy repository connection without manual configuration
- Automatic deployments when you push to your repository
- No need to manually upload code or set up SSH keys
Account Verification: Render typically requires email verification and may also require a credit card for verification (even for free tier usage). This helps prevent abuse of their services.
3. Create a PostgreSQL Database Instance
Now, let's create a PostgreSQL database that our application will use in production:
- From your Render dashboard, click "New +" and select "PostgreSQL"
- Give your database a name (e.g., "app-academy-projects")
- Choose the region closest to you
- Leave the other fields with their default values
- Click "Create Database"
After the database is created (which may take a few minutes), Render will display important information about your database, including:
- The hostname
- The username and password
- Connection URLs
Take note of the "Internal Database URL" - we'll need this later.
PostgreSQL vs. SQLite: In development, we used SQLite because it's simple (just a file) and requires no setup. For production, we're switching to PostgreSQL because:
- PostgreSQL handles concurrent connections better
- It's more robust for production workloads
- It offers better performance and reliability
- It supports more advanced features (like JSON fields, full-text search, etc.)
Database Connection URL Format: The Internal Database URL will look like this:
postgres://username:password@hostname:port/database_name
Internal vs. External URL: Render provides two URLs:
- Internal URL: For connections from other Render services (faster, more secure)
- External URL: For connections from outside Render (useful for database management tools)
We'll use the Internal URL since our web service will also be on Render.
4. Create a New Web Service
Next, let's create a web service to host our Express application:
- From your Render dashboard, click "New +" and select "Web Service"
- Connect your GitHub repository
- If you don't see your repository, click "Configure Account" for GitHub in the sidebar to connect your GitHub account
- Find your project repository and click "Connect"
Web Service Types on Render: Render offers different service types:
- Web Services: For APIs, websites, and applications (what we're using)
- Static Sites: For HTML/CSS/JS websites with no server-side code
- Background Workers: For long-running tasks and job processing
- Cron Jobs: For scheduled tasks
We're using a Web Service because our Express application is a dynamic API that handles HTTP requests and interacts with a database.
Repository Connection: When you connect a GitHub repository, Render sets up a webhook to be notified of new commits. This enables the automatic deployment feature.
5. Configure the Web Service
Now, configure the web service with the following settings:
- Name: Choose a name for your service (this will be part of your URL)
- Region: Choose the same region as your database
- Branch: main
- Root Directory: Leave blank (default is the root of your repo)
- Environment: Node
- Build Command: Enter the following command (all on one line):
npm install && npm run build && npm run sequelize --prefix backend db:seed:undo:all && npm run sequelize --prefix backend db:migrate:undo:all && npm run sequelize --prefix backend db:migrate && npm run sequelize --prefix backend db:seed:all
This build command:
- Installs dependencies
- Runs the build script
- Undoes all seed data and migrations
- Runs all migrations to set up the database schema
- Seeds the database with initial data
Note: This build command is designed for development and testing. For a final production deployment, you might want to remove the "undo" steps to preserve your data:
npm install && npm run build && npm run sequelize --prefix backend db:migrate && npm run sequelize --prefix backend db:seed:all
- Start Command: npm start
Build Command Breakdown: Let's analyze the complete build command:
npm install: Installs root dependencies and (via the install script) backend dependenciesnpm run build: Runs the build script, which calls the backend build script for production compilationnpm run sequelize --prefix backend db:seed:undo:all: Reverses all seed datanpm run sequelize --prefix backend db:migrate:undo:all: Reverses all migrationsnpm run sequelize --prefix backend db:migrate: Runs all migrations to set up tablesnpm run sequelize --prefix backend db:seed:all: Seeds the database with initial data
Why Undo Migrations and Seeds? This approach ensures a clean slate for every deployment, which is useful during development and testing. It effectively resets the database structure and initial data.
Production Considerations: For a real production application with user data, you would:
- Remove the undo steps to preserve existing data
- Use migration files that add/modify tables without data loss
- Consider using a separate seed process for initial setup only
Region Selection: Placing your web service in the same region as your database reduces latency for database operations. This improves performance, especially for database-intensive applications.
6. Set Up Environment Variables
Scroll down to the "Environment Variables" section and add the following variables:
- JWT_SECRET: Click "Generate" to create a secure random value
- JWT_EXPIRES_IN: 604800 (or the value from your local .env file)
- NODE_ENV: production
- SCHEMA: Choose a custom schema name (use snake_case)
- DATABASE_URL: Copy the "Internal Database URL" from your PostgreSQL database instance
Make sure "Auto-Deploy" is set to "Yes" in the Advanced section. This will automatically redeploy your application when you push to the main branch.
Environment Variables in Production: Environment variables serve several important purposes:
- Keep sensitive information out of your codebase
- Configure environment-specific behavior
- Simplify deployment to different environments
Key Variables Explained:
- JWT_SECRET: Used to sign and verify JWTs; must be kept secret and should be different between environments
- JWT_EXPIRES_IN: Token lifetime in seconds (604800 = 7 days)
- NODE_ENV=production: Tells Express to run in production mode (optimized for performance, less verbose errors)
- SCHEMA: Used by Sequelize to namespace your tables in the PostgreSQL database
- DATABASE_URL: Connection string for the PostgreSQL database
Schema in PostgreSQL: In PostgreSQL, a schema is a namespace that contains database objects like tables. Using a custom schema helps:
- Organize tables logically
- Avoid name conflicts with other applications using the same database
- Simplify access control and permissions
Auto-Deploy: This feature automatically triggers a new deployment when changes are pushed to the specified branch (main). It streamlines the development workflow by eliminating manual deployment steps.
7. Deploy the Application
Click "Create Web Service" to start the deployment process. This will take some time (usually 10-15 minutes) as Render:
- Builds your application according to the build command
- Sets up the environment variables
- Starts your application using the start command
You can monitor the progress in the logs. Once the deployment is complete, Render will provide a URL where your application is accessible (e.g., https://your-app-name.onrender.com).
Deployment Process on Render: Behind the scenes, Render:
- Provisions a virtual instance for your application
- Clones your GitHub repository
- Sets up the environment (Node.js, environment variables)
- Runs the build command
- Starts your application
- Sets up a domain and SSL certificate
Deployment Logs: The logs are crucial for troubleshooting. They show:
- The output of each build step
- Any errors that occur during deployment
- The standard output (console.log) from your application
SSL Certificates: Render automatically provisions free SSL certificates for all services, ensuring that your application is accessible via HTTPS. This is important for:
- Secure cookie transmission
- Protected data exchange
- Browser compatibility (many modern features require HTTPS)
8. Test the Deployed Application
Let's test our deployed API to make sure everything is working correctly:
- First, test the CSRF token route: https://your-app-name.onrender.com/api/csrf/restore
- You should see a JSON response with a CSRF token
- Use this token to test your login endpoint using a tool like Postman or a fetch request in a browser console
Example fetch request to test login:
fetch('https://your-app-name.onrender.com/api/session', {
method: 'POST',
headers: {
"Content-Type": "application/json", // Set content type
"XSRF-TOKEN": "your-csrf-token-here" // Include CSRF token from cookie
},
body: JSON.stringify({ credential: 'Demo-lition', password: 'password' }) // Login credentials
}).then(res => res.json()).then(data => console.log(data));
Test other endpoints similarly to ensure they're all working correctly.
Systematic Testing Approach: When testing a deployed application, work methodically:
- Test Prerequisites: First test the CSRF token endpoint since other endpoints depend on it
- Test Core Functionality: Then test authentication endpoints (login, signup, session)
- Test Edge Cases: Try invalid credentials, malformed requests, etc.
Cross-Origin Considerations: When testing from a browser console on a different domain, you may encounter CORS issues. Options to handle this:
- Test from the deployed domain's console (if you have a frontend deployed)
- Use Postman or another API testing tool
- Configure CORS in your backend to allow specific origins
Cookie Handling: The fetch example assumes you're testing in a browser that can handle cookies. If using Postman or another tool, you may need to manually handle cookies or use the withCredentials option.
9. Understand Ongoing Maintenance
With Render's free tier, there's an important limitation to be aware of: your PostgreSQL database instance will be deleted after 90 days. To keep your application running, you'll need to create a new database instance before that happens.
Set a calendar reminder for 85 days from now (to give yourself some buffer) with these steps:
- Create a new PostgreSQL database instance on Render
- Update the DATABASE_URL environment variable in your web service with the new URL
- Manually trigger a new deployment
This will ensure your application continues to run smoothly without data loss.
Free Tier Limitations: Render's free tier has several limitations to be aware of:
- PostgreSQL databases expire after 90 days (the main limitation we need to address)
- Limited computational resources (slower performance)
- Services spin down after periods of inactivity (causing slow initial responses)
- Limited bandwidth and storage
Database Migration Process: When creating a new database after 90 days:
- Create the new database instance
- Update the DATABASE_URL environment variable
- Deploy your application, which will run migrations and seeds
Data Persistence Strategy: For applications with important user data, consider:
- Regular database backups (can be manual on free tier)
- Upgrading to a paid tier for persistent databases
- Implementing data export/import functionality
Monitoring: Regularly check your Render dashboard for:
- Service health and uptime
- Error logs and issues
- Resource usage (approaching limits)
- Upcoming database expiration
Review the Solution
We've successfully deployed our Express authentication application to Render! Let's review what we've accomplished:
- Set up a project structure that works well for deployment
- Created a PostgreSQL database instance for production use
- Configured a web service to host our Express application
- Set up environment variables for the production environment
- Created build and start commands to handle deployment
- Successfully deployed and tested our application
- Understood the maintenance requirements for our deployed application
Key Accomplishments Explained:
- Project Structure: Created a root package.json that directs Render to our backend application
- Database Setup: Provisioned a PostgreSQL database and configured our application to use it
- Environment Configuration: Set up production-specific settings via environment variables
- Build Process: Created a build process that prepares the application for production
- Deployment: Successfully deployed the application to a public URL
- Maintenance Plan: Understood and planned for the 90-day database limitation
Skills Acquired: Through this deployment process, you've gained valuable skills in:
- Cloud platform configuration
- Environment management
- Database setup and connection
- Production deployment workflows
- Build and start script configuration
- Application testing in production
Deployment Architecture
Our deployment architecture consists of two main components:
- PostgreSQL Database: Stores our user data and other application information
- Web Service: Runs our Express application, handling HTTP requests and interacting with the database
This is a simple but effective architecture for many web applications. As your application grows, you might consider more complex architectures with separate services for different parts of your application, content delivery networks (CDNs) for static assets, and caching layers for improved performance.
Scaling This Architecture: As your application grows, this architecture can evolve:
BASIC ARCHITECTURE SCALED ARCHITECTURE
------------------ -------------------
┌──────────────┐ ┌──────────────┐ ┌────────┐ ┌──────────────┐ ┌──────────────┐
│ │ │ │ │ │ │ │ │ │
│ Web Service │◄─►│ PostgreSQL │ │ CDN │◄─►│ Web Services │◄─►│ PostgreSQL │
│ │ │ │ │ │ │ (Load Balanced) │ (Replicated) │
└──────────────┘ └──────────────┘ └────────┘ └──────────────┘ └──────────────┘
▲ ▲ ▲
▲ ▲ ▲ ▲
│ │ │ │
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ │ │ │ │ │ │ │
│ Clients │ │ Clients │ │ Cache Layer│ │ Message │
│ │ │ │ │ │ │ Queue │
└──────────────┘ └────────────┘ └────────────┘ └────────────┘
Scaling Options:
- Horizontal Scaling: Add more web service instances behind a load balancer
- Database Replication: Create read replicas for improved query performance
- CDN Integration: Serve static assets through a Content Delivery Network
- Caching: Add Redis or Memcached to cache frequently accessed data
- Message Queues: Handle asynchronous tasks or background processing
Microservices Evolution: As complexity grows, you might split your monolithic application into smaller, focused services:
- Authentication Service
- User Profile Service
- Content Service
- Notification Service
- etc.
Development vs. Production Environments
It's important to understand the key differences between our development and production environments:
| Aspect | Development (Local) | Production (Render) |
|---|---|---|
| Database | SQLite (file-based) | PostgreSQL (server-based) |
| Environment | development | production |
| Hosting | Local machine | Render cloud platform |
| Error Visibility | Detailed (includes stack traces) | Limited (no stack traces) |
| Security Settings | More relaxed (e.g., CORS enabled) | Stricter (e.g., secure cookies required) |
Environment Differences in Detail:
Database:
- Development: SQLite stores data in a single file, ideal for development but limited in concurrent access
- Production: PostgreSQL offers better performance, concurrency, and reliability
Error Handling:
- Development: Verbose errors with stack traces help debugging
- Production: Limited error details prevent exposing sensitive information to users
Performance Optimization:
- Development: Optimized for quick feedback and debugging
- Production: Optimized for response speed and resource efficiency
Security Measures:
- Development: Often lacks HTTPS, has relaxed CORS
- Production: HTTPS required, strict CORS, secure cookies, rate limiting
Code Execution:
- Development: Uses tools like nodemon to auto-restart on changes
- Production: Uses process managers to handle crashes and ensure uptime
Real-world Deployment Considerations
In real-world applications, deployment often involves additional considerations:
- Continuous Integration/Continuous Deployment (CI/CD): Automating testing and deployment to ensure quality and reduce manual work
- Monitoring and Logging: Setting up tools to track application performance and errors
- Scaling: Adjusting resources based on traffic and usage patterns
- Backup and Recovery: Ensuring data is regularly backed up and can be restored if needed
- Security Updates: Keeping dependencies and the environment up to date with security patches
While our deployment is relatively simple, these principles are still important to consider, especially as your application grows.
CI/CD Pipeline Example: A more advanced deployment setup might include:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ │ │ │ │ │ │ │ │ │
│ Developer │──►│ GitHub Repo │──►│ CI Service │──►│ Staging │──►│ Production │
│ Commit │ │ (PR/Merge) │ │ (Tests/Lints) │ Environment │ │ Deployment │
│ │ │ │ │ │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
Monitoring and Alerting: Production applications need monitoring for:
- Performance: Response times, throughput, resource usage
- Errors: Exception tracking, error rates, API failures
- Business Metrics: User signups, active users, conversion rates
- Infrastructure: Server health, database performance, network issues
Tools for Production Management:
- Logging: ELK Stack (Elasticsearch, Logstash, Kibana), Papertrail
- Monitoring: New Relic, Datadog, Prometheus + Grafana
- Error Tracking: Sentry, Rollbar
- Performance: Lighthouse, WebPageTest
- Security: Snyk, OWASP ZAP, npm audit
Common Issues and Solutions
- Build Failures: If your build fails, check the logs for specific errors. Common issues include syntax errors, missing dependencies, or incorrect build commands.
- Database Connection Errors: Verify that your DATABASE_URL environment variable is correct and that your application is properly configured to use it.
- Missing Environment Variables: Double-check that all required environment variables are set correctly in Render.
- CORS Issues: If your frontend application can't connect to your API, you might need to configure CORS settings for the production environment.
- Schema Issues: Make sure your SCHEMA environment variable is set and that your migrations and models are correctly using it.
Troubleshooting Techniques:
Build Failures:
- Examine the specific error message in the build logs
- Try to reproduce the issue locally by running the build command
- Check for differences between your local and production environments
- Verify package versions and compatibility
Database Connection Issues:
// Common error pattern
SequelizeConnectionError: connect ECONNREFUSED 127.0.0.1:5432
Solutions:
- Verify DATABASE_URL is correctly formatted
- Check that the database service is running
- Confirm your application is using the environment variable correctly
- Test database connection separately using a tool like psql
Common CORS Error:
Access to fetch at 'https://your-api.onrender.com/api/session' from origin 'https://your-frontend.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Solutions:
- Configure your backend to allow specific origins:
// In your Express app setup
if (process.env.NODE_ENV === 'production') {
const allowedOrigins = ['https://your-frontend.com'];
app.use(cors({
origin: function(origin, callback) {
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
}
Debugging in Production
When issues occur in production, you have several tools at your disposal:
- Render Logs: Check the logs in your Render dashboard for error messages and application output.
- Manual Testing: Use tools like Postman or browser fetch requests to test endpoints directly.
- Database Inspection: Connect to your PostgreSQL database to verify data and schema.
- Local Replication: Try to replicate the issue locally by setting up similar environment variables.
Remember, debugging in production requires a careful, methodical approach to avoid introducing new issues or causing downtime.
Production Debugging Best Practices:
1. Accessing Render Logs:
- Navigate to your Web Service in the Render dashboard
- Click on the "Logs" tab
- Use the search function to find specific errors
- Check both "Build Logs" and "Runtime Logs"
2. Enhanced Logging: Consider adding more detailed logging in your application:
// Example of enhanced logging
const logRequest = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} - User: ${req.user ? req.user.id : 'anonymous'}`);
next();
};
app.use(logRequest);
3. Database Inspection: You can connect to your Render PostgreSQL database using:
- The psql command line tool
- A GUI tool like pgAdmin or TablePlus
- Use the External Database URL from your Render dashboard
Example psql connection:
psql postgres://username:password@hostname:port/database
4. Safe Testing Techniques:
- Use read-only operations when possible
- Test on staging environments before production
- Make backups before making changes
- Use transactions for database operations
Conclusion
Congratulations! You've successfully built and deployed a complete Express authentication application. This application provides a solid foundation for any web project that requires user authentication. You can now build frontend applications that use this API for authentication, or extend the API with additional features specific to your application.
Throughout this tutorial, you've learned how to:
- Set up an Express application with security middleware
- Create API routes for a RESTful service
- Implement error handling for a robust API
- Build a complete user authentication system with JWT
- Validate user inputs to ensure data quality and security
- Deploy an Express application to a cloud platform
These skills are fundamental to full-stack web development and will serve you well in building a wide variety of web applications.
Key Skills Summary:
Backend Development:
- RESTful API design
- Middleware implementation
- Database interaction with ORM
- Authentication and security
- Error handling
DevOps and Deployment:
- Environment configuration
- Build and deployment processes
- Database setup and migration
- Production troubleshooting
- Cloud platform management
Security Practices:
- Password hashing
- JWT-based authentication
- CSRF protection
- Input validation
- Environment-specific security settings
These skills represent a solid foundation for a backend developer role and provide transferable knowledge for various web development projects.
Next Steps
Now that you have a working authentication API, here are some ways you might extend or build upon it:
- Frontend Development: Build a React or other frontend application that consumes this API
- Additional Features: Add features like password reset, email verification, or social login
- User Profiles: Extend the User model with additional profile information
- Authorization: Implement role-based access control for different types of users
- API Extensions: Add domain-specific API endpoints for your application's core functionality
Whatever path you choose, the authentication system you've built provides a solid foundation for your application's security needs.
Project Extension Ideas:
1. Enhanced Authentication Features:
- Multi-factor Authentication: Add a second authentication factor like SMS or authenticator app
- OAuth Integration: Allow login with Google, Facebook, GitHub, etc.
- Account Recovery: Implement secure password reset and account recovery workflows
- Session Management: Add the ability to view and manage active sessions
2. Frontend Implementation:
- React: Build a SPA with React Router and authentication context
- Mobile: Create a React Native mobile app that uses your API
- Admin Dashboard: Build an admin interface for user management
3. API Extensions:
- Content Management: Add endpoints for creating and managing content
- File Upload: Implement secure file uploading with AWS S3 or similar
- Analytics: Add endpoints for reporting and analytics
- Real-time Features: Integrate WebSockets for chat or notifications
4. Production Enhancements:
- Rate Limiting: Protect against brute force attacks and API abuse
- Caching: Implement Redis or another caching solution
- Logging: Set up comprehensive logging with a service like LogDNA
- Monitoring: Add application performance monitoring
These extensions can transform your authentication API into a full-featured application platform tailored to your specific needs.
Why Deployment Matters: Deployment is the bridge between development and actual users. A well-designed application is useless if users can't access it. The deployment process transforms your local application into a publicly available service.
Development vs. Production Environments: These environments have fundamental differences:
Why Render? There are many hosting platforms available (Heroku, AWS, DigitalOcean, etc.), but Render offers several advantages for this project:
SQLite to PostgreSQL: This transition is necessary because: